Skip to main content

mz_persist_client/internal/
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
10use std::cmp::Ordering;
11use std::collections::BTreeMap;
12use std::fmt::{Debug, Formatter};
13use std::hash::{Hash, Hasher};
14use std::marker::PhantomData;
15use std::str::FromStr;
16use std::sync::Arc;
17
18use bytes::{Buf, Bytes};
19use differential_dataflow::lattice::Lattice;
20use differential_dataflow::trace::Description;
21use mz_ore::cast::CastInto;
22use mz_ore::{halt, soft_panic_or_log};
23use mz_persist::indexed::encoding::{BatchColumnarFormat, BlobTraceBatchPart, BlobTraceUpdates};
24use mz_persist::location::{SeqNo, VersionedData};
25use mz_persist::metrics::ColumnarMetrics;
26use mz_persist_types::schema::SchemaId;
27use mz_persist_types::stats::{PartStats, ProtoStructStats};
28use mz_persist_types::{Codec, Codec64};
29use mz_proto::{IntoRustIfSome, ProtoMapEntry, ProtoType, RustType, TryFromProtoError};
30use proptest::prelude::Arbitrary;
31use proptest::strategy::Strategy;
32use prost::Message;
33use semver::Version;
34use serde::ser::SerializeStruct;
35use serde::{Deserialize, Serialize, Serializer};
36use timely::progress::{Antichain, Timestamp};
37use uuid::Uuid;
38
39use crate::critical::{CriticalReaderId, Opaque};
40use crate::error::{CodecMismatch, CodecMismatchT};
41use crate::internal::metrics::Metrics;
42use crate::internal::paths::{PartialBatchKey, PartialRollupKey};
43use crate::internal::state::{
44    ActiveGc, ActiveRollup, BatchPart, CriticalReaderState, EncodedSchemas, HandleDebugState,
45    HollowBatch, HollowBatchPart, HollowRollup, HollowRun, HollowRunRef, IdempotencyToken,
46    LeasedReaderState, ProtoActiveGc, ProtoActiveRollup, ProtoCompaction, ProtoCriticalReaderState,
47    ProtoEncodedSchemas, ProtoHandleDebugState, ProtoHollowBatch, ProtoHollowBatchPart,
48    ProtoHollowRollup, ProtoHollowRun, ProtoHollowRunRef, ProtoIdHollowBatch, ProtoIdMerge,
49    ProtoIdSpineBatch, ProtoInlineBatchPart, ProtoInlinedDiffs, ProtoLeasedReaderState, ProtoMerge,
50    ProtoRollup, ProtoRunMeta, ProtoRunOrder, ProtoSpineBatch, ProtoSpineId, ProtoStateDiff,
51    ProtoStateField, ProtoStateFieldDiffType, ProtoStateFieldDiffs, ProtoTrace, ProtoU64Antichain,
52    ProtoU64Description, ProtoVersionedData, ProtoWriterState, RunId, RunMeta, RunOrder, RunPart,
53    State, StateCollections, TypedState, WriterState, proto_hollow_batch_part,
54};
55use crate::internal::state_diff::{
56    ProtoStateFieldDiff, ProtoStateFieldDiffsWriter, StateDiff, StateFieldDiff, StateFieldValDiff,
57};
58use crate::internal::trace::{
59    ActiveCompaction, FlatTrace, SpineId, ThinMerge, ThinSpineBatch, Trace,
60};
61use crate::read::{LeasedReaderId, READER_LEASE_DURATION};
62use crate::{PersistConfig, ShardId, WriterId, cfg};
63
64/// A key and value `Schema` of data written to a batch or shard.
65#[derive(Debug)]
66pub struct Schemas<K: Codec, V: Codec> {
67    /// Id under which this schema is registered in the shard's schema registry,
68    /// if any.
69    pub id: Option<SchemaId>,
70    /// Key `Schema`.
71    pub key: Arc<K::Schema>,
72    /// Value `Schema`.
73    pub val: Arc<V::Schema>,
74}
75
76impl<K: Codec, V: Codec> Clone for Schemas<K, V> {
77    fn clone(&self) -> Self {
78        Self {
79            id: self.id,
80            key: Arc::clone(&self.key),
81            val: Arc::clone(&self.val),
82        }
83    }
84}
85
86/// A proto that is decoded on use.
87///
88/// Because of the way prost works, decoding a large protobuf may result in a
89/// number of very short lived allocations in our RustType/ProtoType decode path
90/// (e.g. this happens for a repeated embedded message). Not every use of
91/// persist State needs every transitive bit of it to be decoded, so we opt
92/// certain parts of it (initially stats) to be decoded on use.
93///
94/// This has the dual benefit of only paying for the short-lived allocs when
95/// necessary and also allowing decoding to be gated by a feature flag. The
96/// tradeoffs are that we might decode more than once and that we have to handle
97/// invalid proto errors in more places.
98///
99/// Mechanically, this is accomplished by making the field a proto `bytes` types
100/// instead of `ProtoFoo`. These bytes then contain the serialization of
101/// ProtoFoo. NB: Swapping between the two is actually a forward and backward
102/// compatible change.
103///
104/// > Embedded messages are compatible with bytes if the bytes contain an
105/// > encoded version of the message.
106///
107/// (See <https://protobuf.dev/programming-guides/proto3/#updating>)
108#[derive(Clone, Serialize, Deserialize)]
109pub struct LazyProto<T> {
110    buf: Bytes,
111    _phantom: PhantomData<fn() -> T>,
112}
113
114impl<T: Message + Default + Debug> Debug for LazyProto<T> {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self.decode() {
117            Ok(proto) => Debug::fmt(&proto, f),
118            Err(err) => f
119                .debug_struct(&format!("LazyProto<{}>", std::any::type_name::<T>()))
120                .field("err", &err)
121                .finish(),
122        }
123    }
124}
125
126impl<T> PartialEq for LazyProto<T> {
127    fn eq(&self, other: &Self) -> bool {
128        self.cmp(other) == Ordering::Equal
129    }
130}
131
132impl<T> Eq for LazyProto<T> {}
133
134impl<T> PartialOrd for LazyProto<T> {
135    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
136        Some(self.cmp(other))
137    }
138}
139
140impl<T> Ord for LazyProto<T> {
141    fn cmp(&self, other: &Self) -> Ordering {
142        let LazyProto {
143            buf: self_buf,
144            _phantom: _,
145        } = self;
146        let LazyProto {
147            buf: other_buf,
148            _phantom: _,
149        } = other;
150        self_buf.cmp(other_buf)
151    }
152}
153
154impl<T> Hash for LazyProto<T> {
155    fn hash<H: Hasher>(&self, state: &mut H) {
156        let LazyProto { buf, _phantom } = self;
157        buf.hash(state);
158    }
159}
160
161impl<T: Message + Default> From<&T> for LazyProto<T> {
162    fn from(value: &T) -> Self {
163        let buf = Bytes::from(value.encode_to_vec());
164        LazyProto {
165            buf,
166            _phantom: PhantomData,
167        }
168    }
169}
170
171impl<T: Message + Default> LazyProto<T> {
172    pub fn decode(&self) -> Result<T, prost::DecodeError> {
173        T::decode(&*self.buf)
174    }
175
176    pub fn decode_to<R: RustType<T>>(&self) -> anyhow::Result<R> {
177        Ok(T::decode(&*self.buf)?.into_rust()?)
178    }
179}
180
181impl<T: Message + Default> RustType<Bytes> for LazyProto<T> {
182    fn into_proto(&self) -> Bytes {
183        self.buf.clone()
184    }
185
186    fn from_proto(buf: Bytes) -> Result<Self, TryFromProtoError> {
187        Ok(Self {
188            buf,
189            _phantom: PhantomData,
190        })
191    }
192}
193
194/// Our Proto implementation, Prost, cannot handle unrecognized fields. This means that unexpected
195/// data will be dropped at deserialization time, which means that we can't reliably roundtrip data
196/// from future versions of the code, which causes trouble during upgrades and at other times.
197///
198/// This type works around the issue by defining an unstructured metadata map. Keys are expected to
199/// be well-known strings defined in the code; values are bytes, expected to be encoded protobuf.
200/// (The association between the two is lightly enforced with the affiliated [MetadataKey] type.)
201/// It's safe to add new metadata keys in new versions, since even unrecognized keys can be losslessly
202/// roundtripped. However, if the metadata is not safe for the old version to ignore -- perhaps it
203/// needs to be kept in sync with some other part of the struct -- you will need to use a more
204/// heavyweight migration for it.
205#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
206pub(crate) struct MetadataMap(BTreeMap<String, Bytes>);
207
208/// Associating a field name and an associated Proto message type, for lookup in a metadata map.
209///
210/// It is an error to reuse key names, or to change the type associated with a particular name.
211/// It is polite to choose short names, since they get serialized alongside every struct.
212#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
213pub(crate) struct MetadataKey<V, P = V> {
214    name: &'static str,
215    type_: PhantomData<(V, P)>,
216}
217
218impl<V, P> MetadataKey<V, P> {
219    pub(crate) const fn new(name: &'static str) -> Self {
220        MetadataKey {
221            name,
222            type_: PhantomData,
223        }
224    }
225}
226
227impl serde::Serialize for MetadataMap {
228    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
229    where
230        S: Serializer,
231    {
232        serializer.collect_map(self.0.iter())
233    }
234}
235
236impl MetadataMap {
237    /// Returns true iff no metadata keys have been set.
238    pub fn is_empty(&self) -> bool {
239        self.0.is_empty()
240    }
241
242    /// Serialize and insert a new key into the map, replacing any existing value for the key.
243    pub fn set<V: RustType<P>, P: prost::Message>(&mut self, key: MetadataKey<V, P>, value: V) {
244        self.0.insert(
245            String::from(key.name),
246            Bytes::from(value.into_proto_owned().encode_to_vec()),
247        );
248    }
249
250    /// Deserialize a key from the map, if it is present.
251    pub fn get<V: RustType<P>, P: prost::Message + Default>(
252        &self,
253        key: MetadataKey<V, P>,
254    ) -> Option<V> {
255        let proto = match P::decode(self.0.get(key.name)?.as_ref()) {
256            Ok(decoded) => decoded,
257            Err(err) => {
258                // This should be impossible unless one of the MetadataKey invariants are broken.
259                soft_panic_or_log!(
260                    "error when decoding {key}; was it redefined? {err}",
261                    key = key.name
262                );
263                return None;
264            }
265        };
266
267        match proto.into_rust() {
268            Ok(proto) => Some(proto),
269            Err(err) => {
270                // This should be impossible unless one of the MetadataKey invariants are broken.
271                soft_panic_or_log!(
272                    "error when decoding {key}; was it redefined? {err}",
273                    key = key.name
274                );
275                None
276            }
277        }
278    }
279}
280impl RustType<BTreeMap<String, Bytes>> for MetadataMap {
281    fn into_proto(&self) -> BTreeMap<String, Bytes> {
282        self.0.clone()
283    }
284    fn from_proto(proto: BTreeMap<String, Bytes>) -> Result<Self, TryFromProtoError> {
285        Ok(MetadataMap(proto))
286    }
287}
288
289pub(crate) fn parse_id(id_prefix: &str, id_type: &str, encoded: &str) -> Result<[u8; 16], String> {
290    let uuid_encoded = match encoded.strip_prefix(id_prefix) {
291        Some(x) => x,
292        None => return Err(format!("invalid {} {}: incorrect prefix", id_type, encoded)),
293    };
294    let uuid = Uuid::parse_str(uuid_encoded)
295        .map_err(|err| format!("invalid {} {}: {}", id_type, encoded, err))?;
296    Ok(*uuid.as_bytes())
297}
298
299pub(crate) fn assert_code_can_read_data(code_version: &Version, data_version: &Version) {
300    if !cfg::code_can_read_data(code_version, data_version) {
301        // We can't catch halts, so panic in test, so we can get unit test
302        // coverage.
303        if cfg!(test) {
304            panic!("code at version {code_version} cannot read data with version {data_version}");
305        } else {
306            halt!("code at version {code_version} cannot read data with version {data_version}");
307        }
308    }
309}
310
311impl RustType<String> for LeasedReaderId {
312    fn into_proto(&self) -> String {
313        self.to_string()
314    }
315
316    fn from_proto(proto: String) -> Result<Self, TryFromProtoError> {
317        match proto.parse() {
318            Ok(x) => Ok(x),
319            Err(_) => Err(TryFromProtoError::InvalidShardId(proto)),
320        }
321    }
322}
323
324impl RustType<String> for CriticalReaderId {
325    fn into_proto(&self) -> String {
326        self.to_string()
327    }
328
329    fn from_proto(proto: String) -> Result<Self, TryFromProtoError> {
330        match proto.parse() {
331            Ok(x) => Ok(x),
332            Err(_) => Err(TryFromProtoError::InvalidShardId(proto)),
333        }
334    }
335}
336
337impl RustType<String> for WriterId {
338    fn into_proto(&self) -> String {
339        self.to_string()
340    }
341
342    fn from_proto(proto: String) -> Result<Self, TryFromProtoError> {
343        match proto.parse() {
344            Ok(x) => Ok(x),
345            Err(_) => Err(TryFromProtoError::InvalidShardId(proto)),
346        }
347    }
348}
349
350impl RustType<ProtoEncodedSchemas> for EncodedSchemas {
351    fn into_proto(&self) -> ProtoEncodedSchemas {
352        ProtoEncodedSchemas {
353            key: self.key.clone(),
354            key_data_type: self.key_data_type.clone(),
355            val: self.val.clone(),
356            val_data_type: self.val_data_type.clone(),
357        }
358    }
359
360    fn from_proto(proto: ProtoEncodedSchemas) -> Result<Self, TryFromProtoError> {
361        Ok(EncodedSchemas {
362            key: proto.key,
363            key_data_type: proto.key_data_type,
364            val: proto.val,
365            val_data_type: proto.val_data_type,
366        })
367    }
368}
369
370impl RustType<String> for IdempotencyToken {
371    fn into_proto(&self) -> String {
372        self.to_string()
373    }
374
375    fn from_proto(proto: String) -> Result<Self, TryFromProtoError> {
376        match proto.parse() {
377            Ok(x) => Ok(x),
378            Err(_) => Err(TryFromProtoError::InvalidShardId(proto)),
379        }
380    }
381}
382
383impl RustType<String> for PartialBatchKey {
384    fn into_proto(&self) -> String {
385        self.0.clone()
386    }
387
388    fn from_proto(proto: String) -> Result<Self, TryFromProtoError> {
389        Ok(PartialBatchKey(proto))
390    }
391}
392
393impl RustType<String> for PartialRollupKey {
394    fn into_proto(&self) -> String {
395        self.0.clone()
396    }
397
398    fn from_proto(proto: String) -> Result<Self, TryFromProtoError> {
399        Ok(PartialRollupKey(proto))
400    }
401}
402
403impl<T: Timestamp + Lattice + Codec64> StateDiff<T> {
404    pub fn encode<B>(&self, buf: &mut B)
405    where
406        B: bytes::BufMut,
407    {
408        self.into_proto()
409            .encode(buf)
410            .expect("no required fields means no initialization errors");
411    }
412
413    pub fn decode(build_version: &Version, buf: Bytes) -> Self {
414        let proto = ProtoStateDiff::decode(buf)
415            // We received a State that we couldn't decode. This could happen if
416            // persist messes up backward/forward compatibility, if the durable
417            // data was corrupted, or if operations messes up deployment. In any
418            // case, fail loudly.
419            .expect("internal error: invalid encoded state");
420        let diff = Self::from_proto(proto).expect("internal error: invalid encoded state");
421        assert_code_can_read_data(build_version, &diff.applier_version);
422        diff
423    }
424}
425
426impl<T: Timestamp + Codec64> RustType<ProtoStateDiff> for StateDiff<T> {
427    fn into_proto(&self) -> ProtoStateDiff {
428        // Deconstruct self so we get a compile failure if new fields are added.
429        let StateDiff {
430            applier_version,
431            seqno_from,
432            seqno_to,
433            walltime_ms,
434            latest_rollup_key,
435            rollups,
436            active_rollup,
437            active_gc,
438            hostname,
439            last_gc_req,
440            leased_readers,
441            critical_readers,
442            writers,
443            schemas,
444            since,
445            legacy_batches,
446            hollow_batches,
447            spine_batches,
448            merges,
449        } = self;
450
451        let proto = ProtoStateFieldDiffs::default();
452
453        // Create a writer so we can efficiently encode our data.
454        let mut writer = proto.into_writer();
455
456        field_diffs_into_proto(ProtoStateField::Hostname, hostname, &mut writer);
457        field_diffs_into_proto(ProtoStateField::LastGcReq, last_gc_req, &mut writer);
458        field_diffs_into_proto(ProtoStateField::Rollups, rollups, &mut writer);
459        field_diffs_into_proto(ProtoStateField::ActiveRollup, active_rollup, &mut writer);
460        field_diffs_into_proto(ProtoStateField::ActiveGc, active_gc, &mut writer);
461        field_diffs_into_proto(ProtoStateField::LeasedReaders, leased_readers, &mut writer);
462        field_diffs_into_proto(
463            ProtoStateField::CriticalReaders,
464            critical_readers,
465            &mut writer,
466        );
467        field_diffs_into_proto(ProtoStateField::Writers, writers, &mut writer);
468        field_diffs_into_proto(ProtoStateField::Schemas, schemas, &mut writer);
469        field_diffs_into_proto(ProtoStateField::Since, since, &mut writer);
470        field_diffs_into_proto(ProtoStateField::LegacyBatches, legacy_batches, &mut writer);
471        field_diffs_into_proto(ProtoStateField::HollowBatches, hollow_batches, &mut writer);
472        field_diffs_into_proto(ProtoStateField::SpineBatches, spine_batches, &mut writer);
473        field_diffs_into_proto(ProtoStateField::SpineMerges, merges, &mut writer);
474
475        // After encoding all of our data, convert back into the proto.
476        let field_diffs = writer.into_proto();
477
478        debug_assert_eq!(field_diffs.validate(), Ok(()));
479        ProtoStateDiff {
480            applier_version: applier_version.to_string(),
481            seqno_from: seqno_from.into_proto(),
482            seqno_to: seqno_to.into_proto(),
483            walltime_ms: walltime_ms.into_proto(),
484            latest_rollup_key: latest_rollup_key.into_proto(),
485            field_diffs: Some(field_diffs),
486        }
487    }
488
489    fn from_proto(proto: ProtoStateDiff) -> Result<Self, TryFromProtoError> {
490        let applier_version = if proto.applier_version.is_empty() {
491            // Backward compatibility with versions of ProtoState before we set
492            // this field: if it's missing (empty), assume an infinitely old
493            // version.
494            semver::Version::new(0, 0, 0)
495        } else {
496            semver::Version::parse(&proto.applier_version).map_err(|err| {
497                TryFromProtoError::InvalidSemverVersion(format!(
498                    "invalid applier_version {}: {}",
499                    proto.applier_version, err
500                ))
501            })?
502        };
503        let mut state_diff = StateDiff::new(
504            applier_version,
505            proto.seqno_from.into_rust()?,
506            proto.seqno_to.into_rust()?,
507            proto.walltime_ms,
508            proto.latest_rollup_key.into_rust()?,
509        );
510        if let Some(field_diffs) = proto.field_diffs {
511            // `field_diffs` is decoded from an untrusted blob, so validate it.
512            field_diffs
513                .validate()
514                .map_err(TryFromProtoError::InvalidPersistState)?;
515            for field_diff in field_diffs.iter() {
516                let (field, diff) = field_diff?;
517                match field {
518                    ProtoStateField::Hostname => field_diff_into_rust::<(), String, _, _, _, _>(
519                        diff,
520                        &mut state_diff.hostname,
521                        |()| Ok(()),
522                        |v| v.into_rust(),
523                    )?,
524                    ProtoStateField::LastGcReq => field_diff_into_rust::<(), u64, _, _, _, _>(
525                        diff,
526                        &mut state_diff.last_gc_req,
527                        |()| Ok(()),
528                        |v| v.into_rust(),
529                    )?,
530                    ProtoStateField::ActiveGc => {
531                        field_diff_into_rust::<(), ProtoActiveGc, _, _, _, _>(
532                            diff,
533                            &mut state_diff.active_gc,
534                            |()| Ok(()),
535                            |v| v.into_rust(),
536                        )?
537                    }
538                    ProtoStateField::ActiveRollup => {
539                        field_diff_into_rust::<(), ProtoActiveRollup, _, _, _, _>(
540                            diff,
541                            &mut state_diff.active_rollup,
542                            |()| Ok(()),
543                            |v| v.into_rust(),
544                        )?
545                    }
546                    ProtoStateField::Rollups => {
547                        field_diff_into_rust::<u64, ProtoHollowRollup, _, _, _, _>(
548                            diff,
549                            &mut state_diff.rollups,
550                            |k| k.into_rust(),
551                            |v| v.into_rust(),
552                        )?
553                    }
554                    // MIGRATION: We previously stored rollups as a `SeqNo ->
555                    // string Key` map, but now the value is a `struct
556                    // HollowRollup`.
557                    ProtoStateField::DeprecatedRollups => {
558                        field_diff_into_rust::<u64, String, _, _, _, _>(
559                            diff,
560                            &mut state_diff.rollups,
561                            |k| k.into_rust(),
562                            |v| {
563                                Ok(HollowRollup {
564                                    key: v.into_rust()?,
565                                    encoded_size_bytes: None,
566                                })
567                            },
568                        )?
569                    }
570                    ProtoStateField::LeasedReaders => {
571                        field_diff_into_rust::<String, ProtoLeasedReaderState, _, _, _, _>(
572                            diff,
573                            &mut state_diff.leased_readers,
574                            |k| k.into_rust(),
575                            |v| v.into_rust(),
576                        )?
577                    }
578                    ProtoStateField::CriticalReaders => {
579                        field_diff_into_rust::<String, ProtoCriticalReaderState, _, _, _, _>(
580                            diff,
581                            &mut state_diff.critical_readers,
582                            |k| k.into_rust(),
583                            |v| v.into_rust(),
584                        )?
585                    }
586                    ProtoStateField::Writers => {
587                        field_diff_into_rust::<String, ProtoWriterState, _, _, _, _>(
588                            diff,
589                            &mut state_diff.writers,
590                            |k| k.into_rust(),
591                            |v| v.into_rust(),
592                        )?
593                    }
594                    ProtoStateField::Schemas => {
595                        field_diff_into_rust::<u64, ProtoEncodedSchemas, _, _, _, _>(
596                            diff,
597                            &mut state_diff.schemas,
598                            |k| k.into_rust(),
599                            |v| v.into_rust(),
600                        )?
601                    }
602                    ProtoStateField::Since => {
603                        field_diff_into_rust::<(), ProtoU64Antichain, _, _, _, _>(
604                            diff,
605                            &mut state_diff.since,
606                            |()| Ok(()),
607                            |v| v.into_rust(),
608                        )?
609                    }
610                    ProtoStateField::LegacyBatches => {
611                        field_diff_into_rust::<ProtoHollowBatch, (), _, _, _, _>(
612                            diff,
613                            &mut state_diff.legacy_batches,
614                            |k| k.into_rust(),
615                            |()| Ok(()),
616                        )?
617                    }
618                    ProtoStateField::HollowBatches => {
619                        field_diff_into_rust::<ProtoSpineId, ProtoHollowBatch, _, _, _, _>(
620                            diff,
621                            &mut state_diff.hollow_batches,
622                            |k| k.into_rust(),
623                            |v| v.into_rust(),
624                        )?
625                    }
626                    ProtoStateField::SpineBatches => {
627                        field_diff_into_rust::<ProtoSpineId, ProtoSpineBatch, _, _, _, _>(
628                            diff,
629                            &mut state_diff.spine_batches,
630                            |k| k.into_rust(),
631                            |v| v.into_rust(),
632                        )?
633                    }
634                    ProtoStateField::SpineMerges => {
635                        field_diff_into_rust::<ProtoSpineId, ProtoMerge, _, _, _, _>(
636                            diff,
637                            &mut state_diff.merges,
638                            |k| k.into_rust(),
639                            |v| v.into_rust(),
640                        )?
641                    }
642                }
643            }
644        }
645        Ok(state_diff)
646    }
647}
648
649fn field_diffs_into_proto<K, KP, V, VP>(
650    field: ProtoStateField,
651    diffs: &[StateFieldDiff<K, V>],
652    writer: &mut ProtoStateFieldDiffsWriter,
653) where
654    KP: prost::Message,
655    K: RustType<KP>,
656    VP: prost::Message,
657    V: RustType<VP>,
658{
659    for diff in diffs.iter() {
660        field_diff_into_proto(field, diff, writer);
661    }
662}
663
664fn field_diff_into_proto<K, KP, V, VP>(
665    field: ProtoStateField,
666    diff: &StateFieldDiff<K, V>,
667    writer: &mut ProtoStateFieldDiffsWriter,
668) where
669    KP: prost::Message,
670    K: RustType<KP>,
671    VP: prost::Message,
672    V: RustType<VP>,
673{
674    writer.push_field(field);
675    writer.encode_proto(&diff.key.into_proto());
676    match &diff.val {
677        StateFieldValDiff::Insert(to) => {
678            writer.push_diff_type(ProtoStateFieldDiffType::Insert);
679            writer.encode_proto(&to.into_proto());
680        }
681        StateFieldValDiff::Update(from, to) => {
682            writer.push_diff_type(ProtoStateFieldDiffType::Update);
683            writer.encode_proto(&from.into_proto());
684            writer.encode_proto(&to.into_proto());
685        }
686        StateFieldValDiff::Delete(from) => {
687            writer.push_diff_type(ProtoStateFieldDiffType::Delete);
688            writer.encode_proto(&from.into_proto());
689        }
690    };
691}
692
693fn field_diff_into_rust<KP, VP, K, V, KFn, VFn>(
694    proto: ProtoStateFieldDiff<'_>,
695    diffs: &mut Vec<StateFieldDiff<K, V>>,
696    k_fn: KFn,
697    v_fn: VFn,
698) -> Result<(), TryFromProtoError>
699where
700    KP: prost::Message + Default,
701    VP: prost::Message + Default,
702    KFn: Fn(KP) -> Result<K, TryFromProtoError>,
703    VFn: Fn(VP) -> Result<V, TryFromProtoError>,
704{
705    let val = match proto.diff_type {
706        ProtoStateFieldDiffType::Insert => {
707            let to = VP::decode(proto.to)
708                .map_err(|err| TryFromProtoError::InvalidPersistState(err.to_string()))?;
709            StateFieldValDiff::Insert(v_fn(to)?)
710        }
711        ProtoStateFieldDiffType::Update => {
712            let from = VP::decode(proto.from)
713                .map_err(|err| TryFromProtoError::InvalidPersistState(err.to_string()))?;
714            let to = VP::decode(proto.to)
715                .map_err(|err| TryFromProtoError::InvalidPersistState(err.to_string()))?;
716
717            StateFieldValDiff::Update(v_fn(from)?, v_fn(to)?)
718        }
719        ProtoStateFieldDiffType::Delete => {
720            let from = VP::decode(proto.from)
721                .map_err(|err| TryFromProtoError::InvalidPersistState(err.to_string()))?;
722            StateFieldValDiff::Delete(v_fn(from)?)
723        }
724    };
725    let key = KP::decode(proto.key)
726        .map_err(|err| TryFromProtoError::InvalidPersistState(err.to_string()))?;
727    diffs.push(StateFieldDiff {
728        key: k_fn(key)?,
729        val,
730    });
731    Ok(())
732}
733
734/// The decoded state of [ProtoRollup] for which we have not yet checked
735/// that codecs match the ones in durable state.
736#[derive(Debug)]
737#[cfg_attr(any(test, debug_assertions), derive(Clone, PartialEq))]
738pub struct UntypedState<T> {
739    pub(crate) key_codec: String,
740    pub(crate) val_codec: String,
741    pub(crate) ts_codec: String,
742    pub(crate) diff_codec: String,
743
744    // Important! This T has not been validated, so we can't expose anything in
745    // State that references T until one of the check methods have been called.
746    // Any field on State that doesn't reference T is fair game.
747    state: State<T>,
748}
749
750impl<T: Timestamp + Lattice + Codec64> UntypedState<T> {
751    pub fn seqno(&self) -> SeqNo {
752        self.state.seqno
753    }
754
755    pub fn rollups(&self) -> &BTreeMap<SeqNo, HollowRollup> {
756        &self.state.collections.rollups
757    }
758
759    pub fn latest_rollup(&self) -> (&SeqNo, &HollowRollup) {
760        self.state.latest_rollup()
761    }
762
763    pub fn apply_encoded_diffs<'a, I: IntoIterator<Item = &'a VersionedData>>(
764        &mut self,
765        cfg: &PersistConfig,
766        metrics: &Metrics,
767        diffs: I,
768    ) {
769        // The apply_encoded_diffs might panic if T is not correct. Making this
770        // a silent no-op is far too subtle for my taste, but it's not clear
771        // what else we could do instead.
772        if T::codec_name() != self.ts_codec {
773            return;
774        }
775        self.state.apply_encoded_diffs(cfg, metrics, diffs);
776    }
777
778    pub fn check_codecs<K: Codec, V: Codec, D: Codec64>(
779        self,
780        shard_id: &ShardId,
781    ) -> Result<TypedState<K, V, T, D>, Box<CodecMismatch>> {
782        // Also defensively check that the shard_id on the state we fetched
783        // matches the shard_id we were trying to fetch.
784        assert_eq!(shard_id, &self.state.shard_id);
785        if K::codec_name() != self.key_codec
786            || V::codec_name() != self.val_codec
787            || T::codec_name() != self.ts_codec
788            || D::codec_name() != self.diff_codec
789        {
790            return Err(Box::new(CodecMismatch {
791                requested: (
792                    K::codec_name(),
793                    V::codec_name(),
794                    T::codec_name(),
795                    D::codec_name(),
796                    None,
797                ),
798                actual: (
799                    self.key_codec,
800                    self.val_codec,
801                    self.ts_codec,
802                    self.diff_codec,
803                    None,
804                ),
805            }));
806        }
807        Ok(TypedState {
808            state: self.state,
809            _phantom: PhantomData,
810        })
811    }
812
813    pub(crate) fn check_ts_codec(self, shard_id: &ShardId) -> Result<State<T>, CodecMismatchT> {
814        // Also defensively check that the shard_id on the state we fetched
815        // matches the shard_id we were trying to fetch.
816        assert_eq!(shard_id, &self.state.shard_id);
817        if T::codec_name() != self.ts_codec {
818            return Err(CodecMismatchT {
819                requested: T::codec_name(),
820                actual: self.ts_codec,
821            });
822        }
823        Ok(self.state)
824    }
825
826    pub fn decode(build_version: &Version, buf: impl Buf) -> Self {
827        let proto = ProtoRollup::decode(buf)
828            // We received a State that we couldn't decode. This could happen if
829            // persist messes up backward/forward compatibility, if the durable
830            // data was corrupted, or if operations messes up deployment. In any
831            // case, fail loudly.
832            .expect("internal error: invalid encoded state");
833        let state = Rollup::from_proto(proto)
834            .expect("internal error: invalid encoded state")
835            .state;
836        assert_code_can_read_data(build_version, &state.state.collections.version);
837        state
838    }
839}
840
841impl<K, V, T, D> From<TypedState<K, V, T, D>> for UntypedState<T>
842where
843    K: Codec,
844    V: Codec,
845    T: Codec64,
846    D: Codec64,
847{
848    fn from(typed_state: TypedState<K, V, T, D>) -> Self {
849        UntypedState {
850            key_codec: K::codec_name(),
851            val_codec: V::codec_name(),
852            ts_codec: T::codec_name(),
853            diff_codec: D::codec_name(),
854            state: typed_state.state,
855        }
856    }
857}
858
859/// A struct that maps 1:1 with ProtoRollup.
860///
861/// Contains State, and optionally the diffs between (state.latest_rollup.seqno, state.seqno]
862///
863/// `diffs` is always expected to be `Some` when writing new rollups, but may be `None`
864/// when deserializing rollups that were persisted before we started inlining diffs.
865#[derive(Debug)]
866pub struct Rollup<T> {
867    pub(crate) state: UntypedState<T>,
868    pub(crate) diffs: Option<InlinedDiffs>,
869}
870
871impl<T: Timestamp + Lattice + Codec64> Rollup<T> {
872    /// Creates a `StateRollup` from a state and diffs from its last rollup.
873    ///
874    /// The diffs must span the seqno range `(state.last_rollup().seqno, state.seqno]`.
875    pub(crate) fn from(state: UntypedState<T>, diffs: Vec<VersionedData>) -> Self {
876        let latest_rollup_seqno = *state.latest_rollup().0;
877        let mut verify_seqno = latest_rollup_seqno;
878        for diff in &diffs {
879            assert_eq!(verify_seqno.next(), diff.seqno);
880            verify_seqno = diff.seqno;
881        }
882        assert_eq!(verify_seqno, state.seqno());
883
884        let diffs = Some(InlinedDiffs::from(
885            latest_rollup_seqno.next(),
886            state.seqno().next(),
887            diffs,
888        ));
889
890        Self { state, diffs }
891    }
892
893    pub(crate) fn from_untyped_state_without_diffs(state: UntypedState<T>) -> Self {
894        Self { state, diffs: None }
895    }
896
897    pub(crate) fn from_state_without_diffs(
898        state: State<T>,
899        key_codec: String,
900        val_codec: String,
901        ts_codec: String,
902        diff_codec: String,
903    ) -> Self {
904        Self::from_untyped_state_without_diffs(UntypedState {
905            key_codec,
906            val_codec,
907            ts_codec,
908            diff_codec,
909            state,
910        })
911    }
912}
913
914#[derive(Debug)]
915pub(crate) struct InlinedDiffs {
916    pub(crate) lower: SeqNo,
917    pub(crate) upper: SeqNo,
918    pub(crate) diffs: Vec<VersionedData>,
919}
920
921impl InlinedDiffs {
922    pub(crate) fn description(&self) -> Description<SeqNo> {
923        Description::new(
924            Antichain::from_elem(self.lower),
925            Antichain::from_elem(self.upper),
926            Antichain::from_elem(SeqNo::minimum()),
927        )
928    }
929
930    fn from(lower: SeqNo, upper: SeqNo, diffs: Vec<VersionedData>) -> Self {
931        for diff in &diffs {
932            assert!(diff.seqno >= lower);
933            assert!(diff.seqno < upper);
934        }
935        Self {
936            lower,
937            upper,
938            diffs,
939        }
940    }
941}
942
943impl RustType<ProtoInlinedDiffs> for InlinedDiffs {
944    fn into_proto(&self) -> ProtoInlinedDiffs {
945        ProtoInlinedDiffs {
946            lower: self.lower.into_proto(),
947            upper: self.upper.into_proto(),
948            diffs: self.diffs.into_proto(),
949        }
950    }
951
952    fn from_proto(proto: ProtoInlinedDiffs) -> Result<Self, TryFromProtoError> {
953        Ok(Self {
954            lower: proto.lower.into_rust()?,
955            upper: proto.upper.into_rust()?,
956            diffs: proto.diffs.into_rust()?,
957        })
958    }
959}
960
961impl<T: Timestamp + Lattice + Codec64> RustType<ProtoRollup> for Rollup<T> {
962    fn into_proto(&self) -> ProtoRollup {
963        ProtoRollup {
964            applier_version: self.state.state.collections.version.to_string(),
965            shard_id: self.state.state.shard_id.into_proto(),
966            seqno: self.state.state.seqno.into_proto(),
967            walltime_ms: self.state.state.walltime_ms.into_proto(),
968            hostname: self.state.state.hostname.into_proto(),
969            key_codec: self.state.key_codec.into_proto(),
970            val_codec: self.state.val_codec.into_proto(),
971            ts_codec: T::codec_name(),
972            diff_codec: self.state.diff_codec.into_proto(),
973            last_gc_req: self.state.state.collections.last_gc_req.into_proto(),
974            active_rollup: self.state.state.collections.active_rollup.into_proto(),
975            active_gc: self.state.state.collections.active_gc.into_proto(),
976            rollups: self
977                .state
978                .state
979                .collections
980                .rollups
981                .iter()
982                .map(|(seqno, key)| (seqno.into_proto(), key.into_proto()))
983                .collect(),
984            deprecated_rollups: Default::default(),
985            leased_readers: self
986                .state
987                .state
988                .collections
989                .leased_readers
990                .iter()
991                .map(|(id, state)| (id.into_proto(), state.into_proto()))
992                .collect(),
993            critical_readers: self
994                .state
995                .state
996                .collections
997                .critical_readers
998                .iter()
999                .map(|(id, state)| (id.into_proto(), state.into_proto()))
1000                .collect(),
1001            writers: self
1002                .state
1003                .state
1004                .collections
1005                .writers
1006                .iter()
1007                .map(|(id, state)| (id.into_proto(), state.into_proto()))
1008                .collect(),
1009            schemas: self
1010                .state
1011                .state
1012                .collections
1013                .schemas
1014                .iter()
1015                .map(|(id, schema)| (id.into_proto(), schema.into_proto()))
1016                .collect(),
1017            trace: Some(self.state.state.collections.trace.into_proto()),
1018            diffs: self.diffs.as_ref().map(|x| x.into_proto()),
1019        }
1020    }
1021
1022    fn from_proto(x: ProtoRollup) -> Result<Self, TryFromProtoError> {
1023        let applier_version = if x.applier_version.is_empty() {
1024            // Backward compatibility with versions of ProtoState before we set
1025            // this field: if it's missing (empty), assume an infinitely old
1026            // version.
1027            semver::Version::new(0, 0, 0)
1028        } else {
1029            semver::Version::parse(&x.applier_version).map_err(|err| {
1030                TryFromProtoError::InvalidSemverVersion(format!(
1031                    "invalid applier_version {}: {}",
1032                    x.applier_version, err
1033                ))
1034            })?
1035        };
1036
1037        let mut rollups = BTreeMap::new();
1038        for (seqno, rollup) in x.rollups {
1039            rollups.insert(seqno.into_rust()?, rollup.into_rust()?);
1040        }
1041        for (seqno, key) in x.deprecated_rollups {
1042            rollups.insert(
1043                seqno.into_rust()?,
1044                HollowRollup {
1045                    key: key.into_rust()?,
1046                    encoded_size_bytes: None,
1047                },
1048            );
1049        }
1050        let mut leased_readers = BTreeMap::new();
1051        for (id, state) in x.leased_readers {
1052            leased_readers.insert(id.into_rust()?, state.into_rust()?);
1053        }
1054        let mut critical_readers = BTreeMap::new();
1055        for (id, state) in x.critical_readers {
1056            critical_readers.insert(id.into_rust()?, state.into_rust()?);
1057        }
1058        let mut writers = BTreeMap::new();
1059        for (id, state) in x.writers {
1060            writers.insert(id.into_rust()?, state.into_rust()?);
1061        }
1062        let mut schemas = BTreeMap::new();
1063        for (id, x) in x.schemas {
1064            schemas.insert(id.into_rust()?, x.into_rust()?);
1065        }
1066        let active_rollup = x
1067            .active_rollup
1068            .map(|rollup| rollup.into_rust())
1069            .transpose()?;
1070        let active_gc = x.active_gc.map(|gc| gc.into_rust()).transpose()?;
1071        let collections = StateCollections {
1072            version: applier_version.clone(),
1073            rollups,
1074            active_rollup,
1075            active_gc,
1076            last_gc_req: x.last_gc_req.into_rust()?,
1077            leased_readers,
1078            critical_readers,
1079            writers,
1080            schemas,
1081            trace: x.trace.into_rust_if_some("trace")?,
1082        };
1083        let state = State {
1084            shard_id: x.shard_id.into_rust()?,
1085            seqno: x.seqno.into_rust()?,
1086            walltime_ms: x.walltime_ms,
1087            hostname: x.hostname,
1088            collections,
1089        };
1090
1091        let diffs: Option<InlinedDiffs> = x.diffs.map(|diffs| diffs.into_rust()).transpose()?;
1092        if let Some(diffs) = &diffs {
1093            // `latest_rollup` below `.expect()`s a rollup to exist, so a proto
1094            // with diffs but no rollups would panic. (A rollup-less state
1095            // without diffs skips this block and decodes fine.)
1096            if state.collections.rollups.is_empty() {
1097                return Err(TryFromProtoError::InvalidPersistState(
1098                    "rollup state has diffs but no rollups".into(),
1099                ));
1100            }
1101            if diffs.lower != state.latest_rollup().0.next() {
1102                return Err(TryFromProtoError::InvalidPersistState(format!(
1103                    "diffs lower ({}) should match latest rollup's successor: ({})",
1104                    diffs.lower,
1105                    state.latest_rollup().0.next()
1106                )));
1107            }
1108            if diffs.upper != state.seqno.next() {
1109                return Err(TryFromProtoError::InvalidPersistState(format!(
1110                    "diffs upper ({}) should match state's successor: ({})",
1111                    diffs.lower,
1112                    state.seqno.next()
1113                )));
1114            }
1115        }
1116
1117        Ok(Rollup {
1118            state: UntypedState {
1119                state,
1120                key_codec: x.key_codec.into_rust()?,
1121                val_codec: x.val_codec.into_rust()?,
1122                ts_codec: x.ts_codec.into_rust()?,
1123                diff_codec: x.diff_codec.into_rust()?,
1124            },
1125            diffs,
1126        })
1127    }
1128}
1129
1130impl RustType<ProtoVersionedData> for VersionedData {
1131    fn into_proto(&self) -> ProtoVersionedData {
1132        ProtoVersionedData {
1133            seqno: self.seqno.into_proto(),
1134            data: Bytes::clone(&self.data),
1135        }
1136    }
1137
1138    fn from_proto(proto: ProtoVersionedData) -> Result<Self, TryFromProtoError> {
1139        Ok(Self {
1140            seqno: proto.seqno.into_rust()?,
1141            data: proto.data,
1142        })
1143    }
1144}
1145
1146impl RustType<ProtoSpineId> for SpineId {
1147    fn into_proto(&self) -> ProtoSpineId {
1148        ProtoSpineId {
1149            lo: self.0.into_proto(),
1150            hi: self.1.into_proto(),
1151        }
1152    }
1153
1154    fn from_proto(proto: ProtoSpineId) -> Result<Self, TryFromProtoError> {
1155        Ok(SpineId(proto.lo.into_rust()?, proto.hi.into_rust()?))
1156    }
1157}
1158
1159impl<T: Timestamp + Codec64> ProtoMapEntry<SpineId, Arc<HollowBatch<T>>> for ProtoIdHollowBatch {
1160    fn from_rust<'a>(entry: (&'a SpineId, &'a Arc<HollowBatch<T>>)) -> Self {
1161        let (id, batch) = entry;
1162        ProtoIdHollowBatch {
1163            id: Some(id.into_proto()),
1164            batch: Some(batch.into_proto()),
1165        }
1166    }
1167
1168    fn into_rust(self) -> Result<(SpineId, Arc<HollowBatch<T>>), TryFromProtoError> {
1169        let id = self.id.into_rust_if_some("ProtoIdHollowBatch::id")?;
1170        let batch = Arc::new(self.batch.into_rust_if_some("ProtoIdHollowBatch::batch")?);
1171        Ok((id, batch))
1172    }
1173}
1174
1175impl<T: Timestamp + Codec64> RustType<ProtoSpineBatch> for ThinSpineBatch<T> {
1176    fn into_proto(&self) -> ProtoSpineBatch {
1177        ProtoSpineBatch {
1178            desc: Some(self.desc.into_proto()),
1179            parts: self.parts.into_proto(),
1180            level: self.level.into_proto(),
1181            descs: self.descs.into_proto(),
1182        }
1183    }
1184
1185    fn from_proto(proto: ProtoSpineBatch) -> Result<Self, TryFromProtoError> {
1186        let level = proto.level.into_rust()?;
1187        let desc = proto.desc.into_rust_if_some("ProtoSpineBatch::desc")?;
1188        let parts = proto.parts.into_rust()?;
1189        let descs = proto.descs.into_rust()?;
1190        Ok(ThinSpineBatch {
1191            level,
1192            desc,
1193            parts,
1194            descs,
1195        })
1196    }
1197}
1198
1199impl<T: Timestamp + Codec64> ProtoMapEntry<SpineId, ThinSpineBatch<T>> for ProtoIdSpineBatch {
1200    fn from_rust<'a>(entry: (&'a SpineId, &'a ThinSpineBatch<T>)) -> Self {
1201        let (id, batch) = entry;
1202        ProtoIdSpineBatch {
1203            id: Some(id.into_proto()),
1204            batch: Some(batch.into_proto()),
1205        }
1206    }
1207
1208    fn into_rust(self) -> Result<(SpineId, ThinSpineBatch<T>), TryFromProtoError> {
1209        let id = self.id.into_rust_if_some("ProtoHollowBatch::id")?;
1210        let batch = self.batch.into_rust_if_some("ProtoHollowBatch::batch")?;
1211        Ok((id, batch))
1212    }
1213}
1214
1215impl RustType<ProtoCompaction> for ActiveCompaction {
1216    fn into_proto(&self) -> ProtoCompaction {
1217        ProtoCompaction {
1218            start_ms: self.start_ms,
1219        }
1220    }
1221
1222    fn from_proto(proto: ProtoCompaction) -> Result<Self, TryFromProtoError> {
1223        Ok(Self {
1224            start_ms: proto.start_ms,
1225        })
1226    }
1227}
1228
1229impl<T: Timestamp + Codec64> RustType<ProtoMerge> for ThinMerge<T> {
1230    fn into_proto(&self) -> ProtoMerge {
1231        ProtoMerge {
1232            since: Some(self.since.into_proto()),
1233            remaining_work: self.remaining_work.into_proto(),
1234            active_compaction: self.active_compaction.into_proto(),
1235        }
1236    }
1237
1238    fn from_proto(proto: ProtoMerge) -> Result<Self, TryFromProtoError> {
1239        let since = proto.since.into_rust_if_some("ProtoMerge::since")?;
1240        let remaining_work = proto.remaining_work.into_rust()?;
1241        let active_compaction = proto.active_compaction.into_rust()?;
1242        Ok(Self {
1243            since,
1244            remaining_work,
1245            active_compaction,
1246        })
1247    }
1248}
1249
1250impl<T: Timestamp + Codec64> ProtoMapEntry<SpineId, ThinMerge<T>> for ProtoIdMerge {
1251    fn from_rust<'a>((id, merge): (&'a SpineId, &'a ThinMerge<T>)) -> Self {
1252        ProtoIdMerge {
1253            id: Some(id.into_proto()),
1254            merge: Some(merge.into_proto()),
1255        }
1256    }
1257
1258    fn into_rust(self) -> Result<(SpineId, ThinMerge<T>), TryFromProtoError> {
1259        let id = self.id.into_rust_if_some("ProtoIdMerge::id")?;
1260        let merge = self.merge.into_rust_if_some("ProtoIdMerge::merge")?;
1261        Ok((id, merge))
1262    }
1263}
1264
1265impl<T: Timestamp + Codec64> RustType<ProtoTrace> for FlatTrace<T> {
1266    fn into_proto(&self) -> ProtoTrace {
1267        let since = self.since.into_proto();
1268        let legacy_batches = self
1269            .legacy_batches
1270            .iter()
1271            .map(|(b, _)| b.into_proto())
1272            .collect();
1273        let hollow_batches = self.hollow_batches.into_proto();
1274        let spine_batches = self.spine_batches.into_proto();
1275        let merges = self.merges.into_proto();
1276        ProtoTrace {
1277            since: Some(since),
1278            legacy_batches,
1279            hollow_batches,
1280            spine_batches,
1281            merges,
1282        }
1283    }
1284
1285    fn from_proto(proto: ProtoTrace) -> Result<Self, TryFromProtoError> {
1286        let since = proto.since.into_rust_if_some("ProtoTrace::since")?;
1287        let legacy_batches = proto
1288            .legacy_batches
1289            .into_iter()
1290            .map(|b| b.into_rust().map(|b| (b, ())))
1291            .collect::<Result<_, _>>()?;
1292        let hollow_batches = proto.hollow_batches.into_rust()?;
1293        let spine_batches = proto.spine_batches.into_rust()?;
1294        let merges = proto.merges.into_rust()?;
1295        Ok(FlatTrace {
1296            since,
1297            legacy_batches,
1298            hollow_batches,
1299            spine_batches,
1300            merges,
1301        })
1302    }
1303}
1304
1305impl<T: Timestamp + Lattice + Codec64> RustType<ProtoTrace> for Trace<T> {
1306    fn into_proto(&self) -> ProtoTrace {
1307        self.flatten().into_proto()
1308    }
1309
1310    fn from_proto(proto: ProtoTrace) -> Result<Self, TryFromProtoError> {
1311        Trace::unflatten(proto.into_rust()?).map_err(TryFromProtoError::InvalidPersistState)
1312    }
1313}
1314
1315impl<T: Timestamp + Codec64> RustType<ProtoLeasedReaderState> for LeasedReaderState<T> {
1316    fn into_proto(&self) -> ProtoLeasedReaderState {
1317        ProtoLeasedReaderState {
1318            seqno: self.seqno.into_proto(),
1319            since: Some(self.since.into_proto()),
1320            last_heartbeat_timestamp_ms: self.last_heartbeat_timestamp_ms.into_proto(),
1321            lease_duration_ms: self.lease_duration_ms.into_proto(),
1322            debug: Some(self.debug.into_proto()),
1323        }
1324    }
1325
1326    fn from_proto(proto: ProtoLeasedReaderState) -> Result<Self, TryFromProtoError> {
1327        let mut lease_duration_ms = proto.lease_duration_ms.into_rust()?;
1328        // MIGRATION: If the lease_duration_ms is empty, then the proto field
1329        // was missing and we need to fill in a default. This would ideally be
1330        // based on the actual value in PersistConfig, but it's only here for a
1331        // short time and this is way easier.
1332        if lease_duration_ms == 0 {
1333            lease_duration_ms = u64::try_from(READER_LEASE_DURATION.default().as_millis())
1334                .expect("lease duration as millis should fit within u64");
1335        }
1336        // MIGRATION: If debug is empty, then the proto field was missing and we
1337        // need to fill in a default.
1338        let debug = proto.debug.unwrap_or_default().into_rust()?;
1339        Ok(LeasedReaderState {
1340            seqno: proto.seqno.into_rust()?,
1341            since: proto
1342                .since
1343                .into_rust_if_some("ProtoLeasedReaderState::since")?,
1344            last_heartbeat_timestamp_ms: proto.last_heartbeat_timestamp_ms.into_rust()?,
1345            lease_duration_ms,
1346            debug,
1347        })
1348    }
1349}
1350
1351impl<T: Timestamp + Codec64> RustType<ProtoCriticalReaderState> for CriticalReaderState<T> {
1352    fn into_proto(&self) -> ProtoCriticalReaderState {
1353        ProtoCriticalReaderState {
1354            since: Some(self.since.into_proto()),
1355            opaque: i64::from_le_bytes(self.opaque.1),
1356            opaque_codec: self.opaque.0.clone(),
1357            debug: Some(self.debug.into_proto()),
1358        }
1359    }
1360
1361    fn from_proto(proto: ProtoCriticalReaderState) -> Result<Self, TryFromProtoError> {
1362        // MIGRATION: If debug is empty, then the proto field was missing and we
1363        // need to fill in a default.
1364        let debug = proto.debug.unwrap_or_default().into_rust()?;
1365        Ok(CriticalReaderState {
1366            since: proto
1367                .since
1368                .into_rust_if_some("ProtoCriticalReaderState::since")?,
1369            opaque: Opaque(proto.opaque_codec, i64::to_le_bytes(proto.opaque)),
1370            debug,
1371        })
1372    }
1373}
1374
1375impl<T: Timestamp + Codec64> RustType<ProtoWriterState> for WriterState<T> {
1376    fn into_proto(&self) -> ProtoWriterState {
1377        ProtoWriterState {
1378            last_heartbeat_timestamp_ms: self.last_heartbeat_timestamp_ms.into_proto(),
1379            lease_duration_ms: self.lease_duration_ms.into_proto(),
1380            most_recent_write_token: self.most_recent_write_token.into_proto(),
1381            most_recent_write_upper: Some(self.most_recent_write_upper.into_proto()),
1382            debug: Some(self.debug.into_proto()),
1383        }
1384    }
1385
1386    fn from_proto(proto: ProtoWriterState) -> Result<Self, TryFromProtoError> {
1387        // MIGRATION: We didn't originally have most_recent_write_token and
1388        // most_recent_write_upper. Pick values that aren't going to
1389        // accidentally match ones in incoming writes and confuse things. We
1390        // could instead use Option on WriterState but this keeps the backward
1391        // compatibility logic confined to one place.
1392        let most_recent_write_token = if proto.most_recent_write_token.is_empty() {
1393            IdempotencyToken::SENTINEL
1394        } else {
1395            proto.most_recent_write_token.into_rust()?
1396        };
1397        let most_recent_write_upper = match proto.most_recent_write_upper {
1398            Some(x) => x.into_rust()?,
1399            None => Antichain::from_elem(T::minimum()),
1400        };
1401        // MIGRATION: If debug is empty, then the proto field was missing and we
1402        // need to fill in a default.
1403        let debug = proto.debug.unwrap_or_default().into_rust()?;
1404        Ok(WriterState {
1405            last_heartbeat_timestamp_ms: proto.last_heartbeat_timestamp_ms.into_rust()?,
1406            lease_duration_ms: proto.lease_duration_ms.into_rust()?,
1407            most_recent_write_token,
1408            most_recent_write_upper,
1409            debug,
1410        })
1411    }
1412}
1413
1414impl RustType<ProtoHandleDebugState> for HandleDebugState {
1415    fn into_proto(&self) -> ProtoHandleDebugState {
1416        ProtoHandleDebugState {
1417            hostname: self.hostname.into_proto(),
1418            purpose: self.purpose.into_proto(),
1419        }
1420    }
1421
1422    fn from_proto(proto: ProtoHandleDebugState) -> Result<Self, TryFromProtoError> {
1423        Ok(HandleDebugState {
1424            hostname: proto.hostname,
1425            purpose: proto.purpose,
1426        })
1427    }
1428}
1429
1430impl<T: Timestamp + Codec64> RustType<ProtoHollowRun> for HollowRun<T> {
1431    fn into_proto(&self) -> ProtoHollowRun {
1432        ProtoHollowRun {
1433            parts: self.parts.into_proto(),
1434        }
1435    }
1436
1437    fn from_proto(proto: ProtoHollowRun) -> Result<Self, TryFromProtoError> {
1438        Ok(HollowRun {
1439            parts: proto.parts.into_rust()?,
1440        })
1441    }
1442}
1443
1444impl<T: Timestamp + Codec64> RustType<ProtoHollowBatch> for HollowBatch<T> {
1445    fn into_proto(&self) -> ProtoHollowBatch {
1446        let mut run_meta = self.run_meta.into_proto();
1447        // For backwards compatibility reasons, don't keep default metadata in the proto.
1448        let run_meta_default = RunMeta::default().into_proto();
1449        while run_meta.last() == Some(&run_meta_default) {
1450            run_meta.pop();
1451        }
1452        ProtoHollowBatch {
1453            desc: Some(self.desc.into_proto()),
1454            parts: self.parts.into_proto(),
1455            len: self.len.into_proto(),
1456            runs: self.run_splits.into_proto(),
1457            run_meta,
1458            deprecated_keys: vec![],
1459        }
1460    }
1461
1462    fn from_proto(proto: ProtoHollowBatch) -> Result<Self, TryFromProtoError> {
1463        let mut parts: Vec<RunPart<T>> = proto.parts.into_rust()?;
1464        // MIGRATION: We used to just have the keys instead of a more structured
1465        // part.
1466        parts.extend(proto.deprecated_keys.into_iter().map(|key| {
1467            RunPart::Single(BatchPart::Hollow(HollowBatchPart {
1468                key: PartialBatchKey(key),
1469                meta: Default::default(),
1470                encoded_size_bytes: 0,
1471                key_lower: vec![],
1472                structured_key_lower: None,
1473                stats: None,
1474                ts_rewrite: None,
1475                diffs_sum: None,
1476                format: None,
1477                schema_id: None,
1478                deprecated_schema_id: None,
1479            }))
1480        }));
1481        // We discard default metadatas from the proto above; re-add them here.
1482        let run_splits: Vec<usize> = proto.runs.into_rust()?;
1483        let num_runs = if parts.is_empty() {
1484            0
1485        } else {
1486            run_splits.len() + 1
1487        };
1488        let mut run_meta: Vec<RunMeta> = proto.run_meta.into_rust()?;
1489        run_meta.resize(num_runs, RunMeta::default());
1490        Ok(HollowBatch {
1491            desc: proto.desc.into_rust_if_some("desc")?,
1492            parts,
1493            len: proto.len.into_rust()?,
1494            run_splits,
1495            run_meta,
1496        })
1497    }
1498}
1499
1500impl RustType<String> for RunId {
1501    fn into_proto(&self) -> String {
1502        self.to_string()
1503    }
1504
1505    fn from_proto(proto: String) -> Result<Self, TryFromProtoError> {
1506        RunId::from_str(&proto).map_err(|_| {
1507            TryFromProtoError::InvalidPersistState(format!("invalid RunId: {}", proto))
1508        })
1509    }
1510}
1511
1512impl RustType<ProtoRunMeta> for RunMeta {
1513    fn into_proto(&self) -> ProtoRunMeta {
1514        let order = match self.order {
1515            None => ProtoRunOrder::Unknown,
1516            Some(RunOrder::Unordered) => ProtoRunOrder::Unordered,
1517            Some(RunOrder::Codec) => ProtoRunOrder::Codec,
1518            Some(RunOrder::Structured) => ProtoRunOrder::Structured,
1519        };
1520        ProtoRunMeta {
1521            order: order.into(),
1522            schema_id: self.schema.into_proto(),
1523            deprecated_schema_id: self.deprecated_schema.into_proto(),
1524            id: self.id.into_proto(),
1525            len: self.len.into_proto(),
1526            meta: self.meta.into_proto(),
1527        }
1528    }
1529
1530    fn from_proto(proto: ProtoRunMeta) -> Result<Self, TryFromProtoError> {
1531        let order = match ProtoRunOrder::try_from(proto.order)? {
1532            ProtoRunOrder::Unknown => None,
1533            ProtoRunOrder::Unordered => Some(RunOrder::Unordered),
1534            ProtoRunOrder::Codec => Some(RunOrder::Codec),
1535            ProtoRunOrder::Structured => Some(RunOrder::Structured),
1536        };
1537        Ok(Self {
1538            order,
1539            schema: proto.schema_id.into_rust()?,
1540            deprecated_schema: proto.deprecated_schema_id.into_rust()?,
1541            id: proto.id.into_rust()?,
1542            len: proto.len.into_rust()?,
1543            meta: proto.meta.into_rust()?,
1544        })
1545    }
1546}
1547
1548impl<T: Timestamp + Codec64> RustType<ProtoHollowBatchPart> for RunPart<T> {
1549    fn into_proto(&self) -> ProtoHollowBatchPart {
1550        match self {
1551            RunPart::Single(part) => part.into_proto(),
1552            RunPart::Many(runs) => runs.into_proto(),
1553        }
1554    }
1555
1556    fn from_proto(proto: ProtoHollowBatchPart) -> Result<Self, TryFromProtoError> {
1557        let run_part = if let Some(proto_hollow_batch_part::Kind::RunRef(_)) = proto.kind {
1558            RunPart::Many(proto.into_rust()?)
1559        } else {
1560            RunPart::Single(proto.into_rust()?)
1561        };
1562        Ok(run_part)
1563    }
1564}
1565
1566impl<T: Timestamp + Codec64> RustType<ProtoHollowBatchPart> for HollowRunRef<T> {
1567    fn into_proto(&self) -> ProtoHollowBatchPart {
1568        let part = ProtoHollowBatchPart {
1569            kind: Some(proto_hollow_batch_part::Kind::RunRef(ProtoHollowRunRef {
1570                key: self.key.into_proto(),
1571                max_part_bytes: self.max_part_bytes.into_proto(),
1572            })),
1573            encoded_size_bytes: self.hollow_bytes.into_proto(),
1574            key_lower: Bytes::copy_from_slice(&self.key_lower),
1575            diffs_sum: self.diffs_sum.map(i64::from_le_bytes),
1576            key_stats: None,
1577            ts_rewrite: None,
1578            format: None,
1579            schema_id: None,
1580            structured_key_lower: self.structured_key_lower.into_proto(),
1581            deprecated_schema_id: None,
1582            metadata: BTreeMap::default(),
1583        };
1584        part
1585    }
1586
1587    fn from_proto(proto: ProtoHollowBatchPart) -> Result<Self, TryFromProtoError> {
1588        let run_proto = match proto.kind {
1589            Some(proto_hollow_batch_part::Kind::RunRef(proto_ref)) => proto_ref,
1590            _ => Err(TryFromProtoError::UnknownEnumVariant(
1591                "ProtoHollowBatchPart::kind".to_string(),
1592            ))?,
1593        };
1594        Ok(Self {
1595            key: run_proto.key.into_rust()?,
1596            hollow_bytes: proto.encoded_size_bytes.into_rust()?,
1597            max_part_bytes: run_proto.max_part_bytes.into_rust()?,
1598            key_lower: proto.key_lower.to_vec(),
1599            structured_key_lower: proto.structured_key_lower.into_rust()?,
1600            diffs_sum: proto.diffs_sum.as_ref().map(|x| i64::to_le_bytes(*x)),
1601            _phantom_data: Default::default(),
1602        })
1603    }
1604}
1605
1606impl<T: Timestamp + Codec64> RustType<ProtoHollowBatchPart> for BatchPart<T> {
1607    fn into_proto(&self) -> ProtoHollowBatchPart {
1608        match self {
1609            BatchPart::Hollow(x) => ProtoHollowBatchPart {
1610                kind: Some(proto_hollow_batch_part::Kind::Key(x.key.into_proto())),
1611                encoded_size_bytes: x.encoded_size_bytes.into_proto(),
1612                key_lower: Bytes::copy_from_slice(&x.key_lower),
1613                structured_key_lower: x.structured_key_lower.as_ref().map(|lazy| lazy.buf.clone()),
1614                key_stats: x.stats.into_proto(),
1615                ts_rewrite: x.ts_rewrite.as_ref().map(|x| x.into_proto()),
1616                diffs_sum: x.diffs_sum.as_ref().map(|x| i64::from_le_bytes(*x)),
1617                format: x.format.map(|f| f.into_proto()),
1618                schema_id: x.schema_id.into_proto(),
1619                deprecated_schema_id: x.deprecated_schema_id.into_proto(),
1620                metadata: BTreeMap::default(),
1621            },
1622            BatchPart::Inline {
1623                updates,
1624                ts_rewrite,
1625                schema_id,
1626                deprecated_schema_id,
1627            } => ProtoHollowBatchPart {
1628                kind: Some(proto_hollow_batch_part::Kind::Inline(updates.into_proto())),
1629                encoded_size_bytes: 0,
1630                key_lower: Bytes::new(),
1631                structured_key_lower: None,
1632                key_stats: None,
1633                ts_rewrite: ts_rewrite.as_ref().map(|x| x.into_proto()),
1634                diffs_sum: None,
1635                format: None,
1636                schema_id: schema_id.into_proto(),
1637                deprecated_schema_id: deprecated_schema_id.into_proto(),
1638                metadata: BTreeMap::default(),
1639            },
1640        }
1641    }
1642
1643    fn from_proto(proto: ProtoHollowBatchPart) -> Result<Self, TryFromProtoError> {
1644        let ts_rewrite = match proto.ts_rewrite {
1645            Some(ts_rewrite) => Some(ts_rewrite.into_rust()?),
1646            None => None,
1647        };
1648        let schema_id = proto.schema_id.into_rust()?;
1649        let deprecated_schema_id = proto.deprecated_schema_id.into_rust()?;
1650        match proto.kind {
1651            Some(proto_hollow_batch_part::Kind::Key(key)) => {
1652                Ok(BatchPart::Hollow(HollowBatchPart {
1653                    key: key.into_rust()?,
1654                    meta: proto.metadata.into_rust()?,
1655                    encoded_size_bytes: proto.encoded_size_bytes.into_rust()?,
1656                    key_lower: proto.key_lower.into(),
1657                    structured_key_lower: proto.structured_key_lower.into_rust()?,
1658                    stats: proto.key_stats.into_rust()?,
1659                    ts_rewrite,
1660                    diffs_sum: proto.diffs_sum.map(i64::to_le_bytes),
1661                    format: proto.format.map(|f| f.into_rust()).transpose()?,
1662                    schema_id,
1663                    deprecated_schema_id,
1664                }))
1665            }
1666            Some(proto_hollow_batch_part::Kind::Inline(x)) => {
1667                // An inline part keeps its data in `kind`; the hollow-only
1668                // fields must be unset. Decoded from an untrusted blob, so
1669                // validate rather than assert.
1670                if proto.encoded_size_bytes != 0
1671                    || !proto.key_lower.is_empty()
1672                    || proto.key_stats.is_some()
1673                    || proto.diffs_sum.is_some()
1674                {
1675                    return Err(TryFromProtoError::InvalidPersistState(
1676                        "inline ProtoHollowBatchPart has hollow-part fields set".into(),
1677                    ));
1678                }
1679                let updates = LazyInlineBatchPart(x.into_rust()?);
1680                Ok(BatchPart::Inline {
1681                    updates,
1682                    ts_rewrite,
1683                    schema_id,
1684                    deprecated_schema_id,
1685                })
1686            }
1687            _ => Err(TryFromProtoError::unknown_enum_variant(
1688                "ProtoHollowBatchPart::kind",
1689            )),
1690        }
1691    }
1692}
1693
1694impl RustType<proto_hollow_batch_part::Format> for BatchColumnarFormat {
1695    fn into_proto(&self) -> proto_hollow_batch_part::Format {
1696        match self {
1697            BatchColumnarFormat::Row => proto_hollow_batch_part::Format::Row(()),
1698            BatchColumnarFormat::Both(version) => {
1699                proto_hollow_batch_part::Format::RowAndColumnar((*version).cast_into())
1700            }
1701            BatchColumnarFormat::Structured => proto_hollow_batch_part::Format::Structured(()),
1702        }
1703    }
1704
1705    fn from_proto(proto: proto_hollow_batch_part::Format) -> Result<Self, TryFromProtoError> {
1706        let format = match proto {
1707            proto_hollow_batch_part::Format::Row(_) => BatchColumnarFormat::Row,
1708            proto_hollow_batch_part::Format::RowAndColumnar(version) => {
1709                BatchColumnarFormat::Both(version.cast_into())
1710            }
1711            proto_hollow_batch_part::Format::Structured(_) => BatchColumnarFormat::Structured,
1712        };
1713        Ok(format)
1714    }
1715}
1716
1717/// Aggregate statistics about data contained in a part.
1718///
1719/// These are "lazy" in the sense that we don't decode them (or even validate
1720/// the encoded version) until they're used.
1721#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1722pub struct LazyPartStats {
1723    key: LazyProto<ProtoStructStats>,
1724}
1725
1726impl Debug for LazyPartStats {
1727    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1728        let mut f = f.debug_tuple("LazyPartStats");
1729        // These bytes come from blob and are never validated on the way in, so
1730        // `Debug` runs on malformed encodings: `Spine::validate` formats the
1731        // whole spine into the message it rejects an untrusted rollup with, and
1732        // `Trace::unflatten` returns that as a decode error. Rendering the
1733        // failure keeps that rejection path a hard error instead of a panic.
1734        match self.try_decode() {
1735            Ok(stats) => f.field(&stats).finish(),
1736            Err(err) => f.field(&format_args!("<undecodable: {err}>")).finish(),
1737        }
1738    }
1739}
1740
1741impl LazyPartStats {
1742    pub(crate) fn encode(x: &PartStats, map_proto: impl FnOnce(&mut ProtoStructStats)) -> Self {
1743        let PartStats { key } = x;
1744        let mut proto_stats = ProtoStructStats::from_rust(key);
1745        map_proto(&mut proto_stats);
1746        LazyPartStats {
1747            key: LazyProto::from(&proto_stats),
1748        }
1749    }
1750    /// Decodes and returns PartStats from the encoded representation.
1751    ///
1752    /// This does not cache the returned value, it decodes each time it's
1753    /// called.
1754    ///
1755    /// The bytes are stored undecoded (see the [RustType] impl) and are never
1756    /// validated on the way in, so a corrupted, crafted, or newer-version blob
1757    /// reaches here intact. There is deliberately no infallible variant: every
1758    /// caller reads stats straight off durable state, where a decode failure
1759    /// must fail open (keep the part, report it selected) rather than panic.
1760    pub fn try_decode(&self) -> Result<PartStats, TryFromProtoError> {
1761        let key = self
1762            .key
1763            .decode()
1764            .map_err(|err| TryFromProtoError::InvalidPersistState(err.to_string()))?;
1765        Ok(PartStats {
1766            key: key.into_rust()?,
1767        })
1768    }
1769}
1770
1771impl RustType<Bytes> for LazyPartStats {
1772    fn into_proto(&self) -> Bytes {
1773        let LazyPartStats { key } = self;
1774        key.into_proto()
1775    }
1776
1777    fn from_proto(proto: Bytes) -> Result<Self, TryFromProtoError> {
1778        Ok(LazyPartStats {
1779            key: proto.into_rust()?,
1780        })
1781    }
1782}
1783
1784#[cfg(test)]
1785pub(crate) fn any_some_lazy_part_stats() -> impl Strategy<Value = Option<LazyPartStats>> {
1786    proptest::prelude::any::<LazyPartStats>().prop_map(Some)
1787}
1788
1789#[allow(unused_parens)]
1790impl Arbitrary for LazyPartStats {
1791    type Parameters = ();
1792    type Strategy =
1793        proptest::strategy::Map<(<PartStats as Arbitrary>::Strategy), fn((PartStats)) -> Self>;
1794
1795    fn arbitrary_with(_: ()) -> Self::Strategy {
1796        Strategy::prop_map((proptest::prelude::any::<PartStats>()), |(x)| {
1797            LazyPartStats::encode(&x, |_| {})
1798        })
1799    }
1800}
1801
1802impl ProtoInlineBatchPart {
1803    pub(crate) fn into_rust<T: Timestamp + Codec64>(
1804        lgbytes: &ColumnarMetrics,
1805        proto: Self,
1806    ) -> Result<BlobTraceBatchPart<T>, TryFromProtoError> {
1807        let updates = proto
1808            .updates
1809            .ok_or_else(|| TryFromProtoError::missing_field("ProtoInlineBatchPart::updates"))?;
1810        let updates = BlobTraceUpdates::from_proto(lgbytes, updates)?;
1811
1812        Ok(BlobTraceBatchPart {
1813            desc: proto.desc.into_rust_if_some("ProtoInlineBatchPart::desc")?,
1814            index: proto.index.into_rust()?,
1815            updates,
1816        })
1817    }
1818}
1819
1820/// A batch part stored inlined in State.
1821#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1822pub struct LazyInlineBatchPart(LazyProto<ProtoInlineBatchPart>);
1823
1824impl From<&ProtoInlineBatchPart> for LazyInlineBatchPart {
1825    fn from(value: &ProtoInlineBatchPart) -> Self {
1826        LazyInlineBatchPart(value.into())
1827    }
1828}
1829
1830impl Serialize for LazyInlineBatchPart {
1831    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1832        // NB: This serialize impl is only used for QA and debugging, so emit a
1833        // truncated version.
1834        let proto = self.0.decode().expect("valid proto");
1835        let mut s = s.serialize_struct("InlineBatchPart", 3)?;
1836        let () = s.serialize_field("desc", &proto.desc)?;
1837        let () = s.serialize_field("index", &proto.index)?;
1838        let () = s.serialize_field("updates[len]", &proto.updates.map_or(0, |x| x.len))?;
1839        s.end()
1840    }
1841}
1842
1843impl LazyInlineBatchPart {
1844    pub(crate) fn encoded_size_bytes(&self) -> usize {
1845        self.0.buf.len()
1846    }
1847
1848    /// Decodes and returns a BlobTraceBatchPart from the encoded
1849    /// representation.
1850    ///
1851    /// This does not cache the returned value, it decodes each time it's
1852    /// called.
1853    pub fn decode<T: Timestamp + Codec64>(
1854        &self,
1855        lgbytes: &ColumnarMetrics,
1856    ) -> Result<BlobTraceBatchPart<T>, TryFromProtoError> {
1857        let proto = self.0.decode().expect("valid proto");
1858        ProtoInlineBatchPart::into_rust(lgbytes, proto)
1859    }
1860}
1861
1862impl RustType<Bytes> for LazyInlineBatchPart {
1863    fn into_proto(&self) -> Bytes {
1864        self.0.into_proto()
1865    }
1866
1867    fn from_proto(proto: Bytes) -> Result<Self, TryFromProtoError> {
1868        Ok(LazyInlineBatchPart(proto.into_rust()?))
1869    }
1870}
1871
1872impl RustType<ProtoHollowRollup> for HollowRollup {
1873    fn into_proto(&self) -> ProtoHollowRollup {
1874        ProtoHollowRollup {
1875            key: self.key.into_proto(),
1876            encoded_size_bytes: self.encoded_size_bytes.into_proto(),
1877        }
1878    }
1879
1880    fn from_proto(proto: ProtoHollowRollup) -> Result<Self, TryFromProtoError> {
1881        Ok(HollowRollup {
1882            key: proto.key.into_rust()?,
1883            encoded_size_bytes: proto.encoded_size_bytes.into_rust()?,
1884        })
1885    }
1886}
1887
1888impl RustType<ProtoActiveRollup> for ActiveRollup {
1889    fn into_proto(&self) -> ProtoActiveRollup {
1890        ProtoActiveRollup {
1891            start_ms: self.start_ms,
1892            seqno: self.seqno.into_proto(),
1893        }
1894    }
1895
1896    fn from_proto(proto: ProtoActiveRollup) -> Result<Self, TryFromProtoError> {
1897        Ok(ActiveRollup {
1898            start_ms: proto.start_ms,
1899            seqno: proto.seqno.into_rust()?,
1900        })
1901    }
1902}
1903
1904impl RustType<ProtoActiveGc> for ActiveGc {
1905    fn into_proto(&self) -> ProtoActiveGc {
1906        ProtoActiveGc {
1907            start_ms: self.start_ms,
1908            seqno: self.seqno.into_proto(),
1909        }
1910    }
1911
1912    fn from_proto(proto: ProtoActiveGc) -> Result<Self, TryFromProtoError> {
1913        Ok(ActiveGc {
1914            start_ms: proto.start_ms,
1915            seqno: proto.seqno.into_rust()?,
1916        })
1917    }
1918}
1919
1920impl<T: Timestamp + Codec64> RustType<ProtoU64Description> for Description<T> {
1921    fn into_proto(&self) -> ProtoU64Description {
1922        ProtoU64Description {
1923            lower: Some(self.lower().into_proto()),
1924            upper: Some(self.upper().into_proto()),
1925            since: Some(self.since().into_proto()),
1926        }
1927    }
1928
1929    fn from_proto(proto: ProtoU64Description) -> Result<Self, TryFromProtoError> {
1930        let lower: Antichain<T> = proto.lower.into_rust_if_some("lower")?;
1931        // `Description::new` asserts a non-empty lower frontier; `lower` comes
1932        // from an untrusted blob, so validate it here instead of panicking.
1933        if lower.elements().is_empty() {
1934            return Err(TryFromProtoError::InvalidPersistState(
1935                "ProtoU64Description has an empty lower frontier".into(),
1936            ));
1937        }
1938        Ok(Description::new(
1939            lower,
1940            proto.upper.into_rust_if_some("upper")?,
1941            proto.since.into_rust_if_some("since")?,
1942        ))
1943    }
1944}
1945
1946impl<T: Timestamp + Codec64> RustType<ProtoU64Antichain> for Antichain<T> {
1947    fn into_proto(&self) -> ProtoU64Antichain {
1948        ProtoU64Antichain {
1949            elements: self
1950                .elements()
1951                .iter()
1952                .map(|x| i64::from_le_bytes(T::encode(x)))
1953                .collect(),
1954        }
1955    }
1956
1957    fn from_proto(proto: ProtoU64Antichain) -> Result<Self, TryFromProtoError> {
1958        let elements = proto
1959            .elements
1960            .iter()
1961            .map(|x| T::decode(x.to_le_bytes()))
1962            .collect::<Vec<_>>();
1963        Ok(Antichain::from(elements))
1964    }
1965}
1966
1967#[cfg(test)]
1968mod tests {
1969    use mz_ore::assert_none;
1970    use mz_persist_types::stats::{ProtoDynStats, ProtoStructStats};
1971
1972    use bytes::Bytes;
1973    use mz_build_info::DUMMY_BUILD_INFO;
1974    use mz_dyncfg::ConfigUpdates;
1975    use mz_ore::assert_err;
1976    use mz_ore::cast::CastFrom;
1977    use mz_persist::location::SeqNo;
1978    use proptest::prelude::*;
1979
1980    use crate::ShardId;
1981    use crate::internal::paths::PartialRollupKey;
1982    use crate::internal::state::tests::any_state;
1983    use crate::internal::state::{BatchPart, HandleDebugState};
1984    use crate::internal::state_diff::StateDiff;
1985    use crate::tests::new_test_client_cache;
1986
1987    use super::*;
1988
1989    #[mz_ore::test]
1990    fn rollup_inline_batch_part_with_hollow_fields_is_error() {
1991        // An inline `ProtoHollowBatchPart` carrying hollow-only fields (here a
1992        // non-zero encoded_size_bytes) must decode to an error, not panic.
1993        // Regression for a rollup_proto_roundtrip fuzz finding.
1994        use mz_proto::ProtoType;
1995        use prost::Message;
1996        let bytes: &[u8] = &[
1997            0x3a, 0x12, 0x0a, 0x00, 0x12, 0x0a, 0x22, 0x06, 0x5a, 0x00, 0x10, 0x02, 0x2a, 0x00,
1998            0x22, 0x00, 0x22, 0x00, 0x22, 0x00,
1999        ];
2000        let proto = crate::internal::state::ProtoRollup::decode(bytes)
2001            .expect("crash input decodes as a proto");
2002        let result: Result<Rollup<u64>, _> = proto.into_rust();
2003        assert_err!(result);
2004    }
2005
2006    #[mz_ore::test]
2007    fn rollup_batch_with_empty_lower_frontier_is_error() {
2008        // A `ProtoU64Description` with an empty lower frontier must decode to an
2009        // error: `Description::new` asserts a non-empty lower. Regression for a
2010        // rollup_proto_roundtrip fuzz finding.
2011        use mz_proto::ProtoType;
2012        use prost::Message;
2013        let bytes: &[u8] = &[
2014            0x3a, 0x12, 0x0a, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x0a, 0x00, 0x12, 0x00, 0x1a, 0x00,
2015            0x12, 0x00, 0x32, 0x00, 0x22, 0x00,
2016        ];
2017        let proto = crate::internal::state::ProtoRollup::decode(bytes)
2018            .expect("crash input decodes as a proto");
2019        let result: Result<Rollup<u64>, _> = proto.into_rust();
2020        assert_err!(result);
2021    }
2022
2023    #[mz_ore::test]
2024    fn metadata_map() {
2025        const COUNT: MetadataKey<u64> = MetadataKey::new("count");
2026
2027        let mut map = MetadataMap::default();
2028        map.set(COUNT, 100);
2029        let mut map = MetadataMap::from_proto(map.into_proto()).unwrap();
2030        assert_eq!(map.get(COUNT), Some(100));
2031
2032        const ANTICHAIN: MetadataKey<Antichain<u64>, ProtoU64Antichain> =
2033            MetadataKey::new("antichain");
2034        assert_none!(map.get(ANTICHAIN));
2035
2036        map.set(ANTICHAIN, Antichain::from_elem(30));
2037        let map = MetadataMap::from_proto(map.into_proto()).unwrap();
2038        assert_eq!(map.get(COUNT), Some(100));
2039        assert_eq!(map.get(ANTICHAIN), Some(Antichain::from_elem(30)));
2040    }
2041
2042    #[mz_ore::test]
2043    fn applier_version_state() {
2044        let v1 = semver::Version::new(1, 0, 0);
2045        let v2 = semver::Version::new(2, 0, 0);
2046        let v3 = semver::Version::new(3, 0, 0);
2047
2048        // Code version v2 evaluates and writes out some State.
2049        let shard_id = ShardId::new();
2050        let state = TypedState::<(), (), u64, i64>::new(v2.clone(), shard_id, "".to_owned(), 0);
2051        let rollup =
2052            Rollup::from_untyped_state_without_diffs(state.clone_for_rollup().into()).into_proto();
2053        let mut buf = Vec::new();
2054        rollup.encode(&mut buf).expect("serializable");
2055        let bytes = Bytes::from(buf);
2056
2057        // We can read it back using persist code v2 and v3.
2058        assert_eq!(
2059            UntypedState::<u64>::decode(&v2, bytes.clone())
2060                .check_codecs(&shard_id)
2061                .as_ref(),
2062            Ok(&state)
2063        );
2064        assert_eq!(
2065            UntypedState::<u64>::decode(&v3, bytes.clone())
2066                .check_codecs(&shard_id)
2067                .as_ref(),
2068            Ok(&state)
2069        );
2070
2071        // But we can't read it back using v1 because v1 might corrupt it by
2072        // losing or misinterpreting something written out by a future version
2073        // of code.
2074        #[allow(clippy::disallowed_methods)] // not using enhanced panic handler in tests
2075        let v1_res = std::panic::catch_unwind(|| UntypedState::<u64>::decode(&v1, bytes.clone()));
2076        assert_err!(v1_res);
2077    }
2078
2079    #[mz_ore::test]
2080    fn applier_version_state_diff() {
2081        let v1 = semver::Version::new(1, 0, 0);
2082        let v2 = semver::Version::new(2, 0, 0);
2083        let v3 = semver::Version::new(3, 0, 0);
2084
2085        // Code version v2 evaluates and writes out some State.
2086        let diff = StateDiff::<u64>::new(
2087            v2.clone(),
2088            SeqNo(0),
2089            SeqNo(1),
2090            2,
2091            PartialRollupKey("rollup".into()),
2092        );
2093        let mut buf = Vec::new();
2094        diff.encode(&mut buf);
2095        let bytes = Bytes::from(buf);
2096
2097        // We can read it back using persist code v2 and v3.
2098        assert_eq!(StateDiff::decode(&v2, bytes.clone()), diff);
2099        assert_eq!(StateDiff::decode(&v3, bytes.clone()), diff);
2100
2101        // But we can't read it back using v1 because v1 might corrupt it by
2102        // losing or misinterpreting something written out by a future version
2103        // of code.
2104        #[allow(clippy::disallowed_methods)] // not using enhanced panic handler in tests
2105        let v1_res = std::panic::catch_unwind(|| StateDiff::<u64>::decode(&v1, bytes));
2106        assert_err!(v1_res);
2107    }
2108
2109    #[mz_ore::test]
2110    fn hollow_batch_migration_keys() {
2111        let x = HollowBatch::new_run(
2112            Description::new(
2113                Antichain::from_elem(1u64),
2114                Antichain::from_elem(2u64),
2115                Antichain::from_elem(3u64),
2116            ),
2117            vec![RunPart::Single(BatchPart::Hollow(HollowBatchPart {
2118                key: PartialBatchKey("a".into()),
2119                meta: Default::default(),
2120                encoded_size_bytes: 5,
2121                key_lower: vec![],
2122                structured_key_lower: None,
2123                stats: None,
2124                ts_rewrite: None,
2125                diffs_sum: None,
2126                format: None,
2127                schema_id: None,
2128                deprecated_schema_id: None,
2129            }))],
2130            4,
2131        );
2132        let mut old = x.into_proto();
2133        // Old ProtoHollowBatch had keys instead of parts.
2134        old.deprecated_keys = vec!["b".into()];
2135        // We don't expect to see a ProtoHollowBatch with keys _and_ parts, only
2136        // one or the other, but we have a defined output, so may as well test
2137        // it.
2138        let mut expected = x;
2139        // We fill in 0 for encoded_size_bytes when we migrate from keys. This
2140        // will violate bounded memory usage compaction during the transition
2141        // (short-term issue), but that's better than creating unnecessary runs
2142        // (longer-term issue).
2143        expected
2144            .parts
2145            .push(RunPart::Single(BatchPart::Hollow(HollowBatchPart {
2146                key: PartialBatchKey("b".into()),
2147                meta: Default::default(),
2148                encoded_size_bytes: 0,
2149                key_lower: vec![],
2150                structured_key_lower: None,
2151                stats: None,
2152                ts_rewrite: None,
2153                diffs_sum: None,
2154                format: None,
2155                schema_id: None,
2156                deprecated_schema_id: None,
2157            })));
2158        assert_eq!(<HollowBatch<u64>>::from_proto(old).unwrap(), expected);
2159    }
2160
2161    #[mz_ore::test]
2162    fn reader_state_migration_lease_duration() {
2163        let x = LeasedReaderState {
2164            seqno: SeqNo(1),
2165            since: Antichain::from_elem(2u64),
2166            last_heartbeat_timestamp_ms: 3,
2167            debug: HandleDebugState {
2168                hostname: "host".to_owned(),
2169                purpose: "purpose".to_owned(),
2170            },
2171            // Old ProtoReaderState had no lease_duration_ms field
2172            lease_duration_ms: 0,
2173        };
2174        let old = x.into_proto();
2175        let mut expected = x;
2176        // We fill in DEFAULT_READ_LEASE_DURATION for lease_duration_ms when we
2177        // migrate from unset.
2178        expected.lease_duration_ms =
2179            u64::try_from(READER_LEASE_DURATION.default().as_millis()).unwrap();
2180        assert_eq!(<LeasedReaderState<u64>>::from_proto(old).unwrap(), expected);
2181    }
2182
2183    #[mz_ore::test]
2184    fn writer_state_migration_most_recent_write() {
2185        let proto = ProtoWriterState {
2186            last_heartbeat_timestamp_ms: 1,
2187            lease_duration_ms: 2,
2188            // Old ProtoWriterState had no most_recent_write_token or
2189            // most_recent_write_upper.
2190            most_recent_write_token: "".into(),
2191            most_recent_write_upper: None,
2192            debug: Some(ProtoHandleDebugState {
2193                hostname: "host".to_owned(),
2194                purpose: "purpose".to_owned(),
2195            }),
2196        };
2197        let expected = WriterState {
2198            last_heartbeat_timestamp_ms: proto.last_heartbeat_timestamp_ms,
2199            lease_duration_ms: proto.lease_duration_ms,
2200            most_recent_write_token: IdempotencyToken::SENTINEL,
2201            most_recent_write_upper: Antichain::from_elem(0),
2202            debug: HandleDebugState {
2203                hostname: "host".to_owned(),
2204                purpose: "purpose".to_owned(),
2205            },
2206        };
2207        assert_eq!(<WriterState<u64>>::from_proto(proto).unwrap(), expected);
2208    }
2209
2210    #[mz_ore::test]
2211    fn state_migration_rollups() {
2212        let r1 = HollowRollup {
2213            key: PartialRollupKey("foo".to_owned()),
2214            encoded_size_bytes: None,
2215        };
2216        let r2 = HollowRollup {
2217            key: PartialRollupKey("bar".to_owned()),
2218            encoded_size_bytes: Some(2),
2219        };
2220        let shard_id = ShardId::new();
2221        let mut state = TypedState::<(), (), u64, i64>::new(
2222            DUMMY_BUILD_INFO.semver_version(),
2223            shard_id,
2224            "host".to_owned(),
2225            0,
2226        );
2227        state.state.collections.rollups.insert(SeqNo(2), r2.clone());
2228        let mut proto = Rollup::from_untyped_state_without_diffs(state.into()).into_proto();
2229
2230        // Manually add the old rollup encoding.
2231        proto.deprecated_rollups.insert(1, r1.key.0.clone());
2232
2233        let state: Rollup<u64> = proto.into_rust().unwrap();
2234        let state = state.state;
2235        let state = state.check_codecs::<(), (), i64>(&shard_id).unwrap();
2236        let expected = vec![(SeqNo(1), r1), (SeqNo(2), r2)];
2237        assert_eq!(
2238            state
2239                .state
2240                .collections
2241                .rollups
2242                .into_iter()
2243                .collect::<Vec<_>>(),
2244            expected
2245        );
2246    }
2247
2248    /// A rollup proto that carries diffs but whose state has no rollups is
2249    /// malformed: the diff bounds are validated against the latest rollup, which
2250    /// `latest_rollup` `.expect()`s to exist. Decoding it must return an error
2251    /// rather than panicking. A rollup-less state *without* diffs is legitimate,
2252    /// as `applier_version_state` relies on, and must still decode. Regression
2253    /// for a rollup_proto_roundtrip fuzz crash.
2254    #[mz_ore::test]
2255    fn rollup_proto_with_diffs_but_no_rollups_is_rejected() {
2256        let shard_id = ShardId::new();
2257        let mut state = TypedState::<(), (), u64, i64>::new(
2258            DUMMY_BUILD_INFO.semver_version(),
2259            shard_id,
2260            "host".to_owned(),
2261            0,
2262        );
2263        // Anchor a rollup at the state's seqno so `Rollup::from` accepts the
2264        // (empty) diff range, producing a proto that does carry diffs.
2265        let seqno = state.state.seqno;
2266        state.state.collections.rollups.insert(
2267            seqno,
2268            HollowRollup {
2269                key: PartialRollupKey("foo".to_owned()),
2270                encoded_size_bytes: None,
2271            },
2272        );
2273        let mut proto = Rollup::from(state.into(), Vec::new()).into_proto();
2274
2275        // Strip every rollup, leaving a proto that has diffs but no rollups.
2276        proto.rollups.clear();
2277        proto.deprecated_rollups.clear();
2278
2279        let result: Result<Rollup<u64>, _> = proto.into_rust();
2280        assert!(
2281            result.is_err(),
2282            "a rollup proto with diffs but no rollups must error, not panic"
2283        );
2284    }
2285
2286    /// Returns a valid rollup proto whose trace was mutated by `update_trace`.
2287    fn rollup_proto_with_trace(update_trace: impl FnOnce(&mut ProtoTrace)) -> ProtoRollup {
2288        let state = TypedState::<(), (), u64, i64>::new(
2289            DUMMY_BUILD_INFO.semver_version(),
2290            ShardId::new(),
2291            "host".to_owned(),
2292            0,
2293        );
2294        let mut proto = Rollup::from_untyped_state_without_diffs(state.into()).into_proto();
2295        update_trace(proto.trace.as_mut().expect("fresh state has a trace"));
2296        proto
2297    }
2298
2299    fn u64_desc_proto(lower: u64, upper: u64, since: u64) -> ProtoU64Description {
2300        Description::new(
2301            Antichain::from_elem(lower),
2302            Antichain::from_elem(upper),
2303            Antichain::from_elem(since),
2304        )
2305        .into_proto()
2306    }
2307
2308    fn legacy_batch_proto(lower: u64, upper: u64, since: u64) -> ProtoHollowBatch {
2309        ProtoHollowBatch {
2310            desc: Some(u64_desc_proto(lower, upper, since)),
2311            ..Default::default()
2312        }
2313    }
2314
2315    fn rollup_decode_err(proto: ProtoRollup) -> String {
2316        let result: Result<Rollup<u64>, _> = proto.into_rust();
2317        match result {
2318            Ok(_) => panic!("crafted rollup proto must fail to decode"),
2319            Err(err) => err.to_string(),
2320        }
2321    }
2322
2323    /// The trace in a rollup proto is decoded from an untrusted blob, and
2324    /// `Trace::unflatten` re-inserts structureless legacy batches into a
2325    /// spine whose (always-on) asserts require them to tile the timeline
2326    /// contiguously from the minimum frontier. A batch that doesn't must fail
2327    /// decoding instead. Regression for a rollup_proto_roundtrip fuzz crash
2328    /// (crash-8603829ee2a3b9c28ee988a14136050d1afe984c).
2329    #[mz_ore::test]
2330    fn rollup_proto_with_noncontiguous_legacy_batches_is_rejected() {
2331        let proto = rollup_proto_with_trace(|trace| {
2332            trace.legacy_batches.push(legacy_batch_proto(39, 40, 0));
2333        });
2334        let err = rollup_decode_err(proto);
2335        assert!(err.contains("legacy batch lower"), "{err}");
2336    }
2337
2338    /// Like [rollup_proto_with_noncontiguous_legacy_batches_is_rejected], but
2339    /// for `Spine::insert`'s other assert: a legacy batch with an empty time
2340    /// range.
2341    #[mz_ore::test]
2342    fn rollup_proto_with_empty_range_legacy_batch_is_rejected() {
2343        let proto = rollup_proto_with_trace(|trace| {
2344            trace.legacy_batches.push(legacy_batch_proto(0, 0, 0));
2345        });
2346        let err = rollup_decode_err(proto);
2347        assert!(err.contains("empty time range"), "{err}");
2348    }
2349
2350    /// A batch whose since is past the trace's since reconstructs without
2351    /// tripping any spine assert but violates `Trace::validate`, which used to
2352    /// run only under `debug_assert`. It must be a decode error.
2353    #[mz_ore::test]
2354    fn rollup_proto_with_batch_since_past_trace_since_is_rejected() {
2355        let proto = rollup_proto_with_trace(|trace| {
2356            trace.legacy_batches.push(legacy_batch_proto(0, 1, 5));
2357        });
2358        let err = rollup_decode_err(proto);
2359        assert!(err.contains("past the spine since"), "{err}");
2360    }
2361
2362    /// An absurd batch len overflows the spine's maintenance arithmetic
2363    /// (`len.next_power_of_two()`, summing merged batch lens). It must be a
2364    /// decode error.
2365    #[mz_ore::test]
2366    fn rollup_proto_with_absurd_batch_len_is_rejected() {
2367        let proto = rollup_proto_with_trace(|trace| {
2368            trace.legacy_batches.push(ProtoHollowBatch {
2369                desc: Some(u64_desc_proto(0, 1, 0)),
2370                len: u64::MAX,
2371                ..Default::default()
2372            });
2373        });
2374        let err = rollup_decode_err(proto);
2375        assert!(err.contains("maximum trace size"), "{err}");
2376    }
2377
2378    /// An absurd spine batch level previously overflowed `level + 1` and sized
2379    /// a giant `vec![]` allocation. It must be a decode error.
2380    #[mz_ore::test]
2381    fn rollup_proto_with_absurd_spine_level_is_rejected() {
2382        let proto = rollup_proto_with_trace(|trace| {
2383            trace.spine_batches.push(ProtoIdSpineBatch {
2384                id: Some(SpineId(0, 1).into_proto()),
2385                batch: Some(ProtoSpineBatch {
2386                    level: u64::MAX,
2387                    desc: Some(u64_desc_proto(0, 1, 0)),
2388                    parts: vec![],
2389                    descs: vec![],
2390                }),
2391            });
2392        });
2393        let err = rollup_decode_err(proto);
2394        assert!(err.contains("exceeds the maximum"), "{err}");
2395    }
2396
2397    /// A spine batch whose parts don't tile its id range (here: no parts at
2398    /// all) previously tripped the `debug_assert`s in `SpineBatch::id`. It
2399    /// must be a decode error.
2400    #[mz_ore::test]
2401    fn rollup_proto_with_partless_spine_batch_is_rejected() {
2402        let proto = rollup_proto_with_trace(|trace| {
2403            trace.spine_batches.push(ProtoIdSpineBatch {
2404                id: Some(SpineId(0, 1).into_proto()),
2405                batch: Some(ProtoSpineBatch {
2406                    level: 0,
2407                    desc: Some(u64_desc_proto(0, 1, 0)),
2408                    parts: vec![],
2409                    descs: vec![],
2410                }),
2411            });
2412        });
2413        let err = rollup_decode_err(proto);
2414        assert!(err.contains("do not tile"), "{err}");
2415    }
2416
2417    /// A spine batch whose parts hit the right endpoints but overlap in the
2418    /// middle (id `[0, 3)` tiled by parts `[0, 2)` and `[1, 3)`) passes the
2419    /// endpoint check but is not a valid tiling. Such parts reach compaction's
2420    /// `id_range`, whose contiguity `assert_eq!` would panic later, so decoding
2421    /// must reject them here instead.
2422    #[mz_ore::test]
2423    fn rollup_proto_with_noncontiguous_spine_parts_is_rejected() {
2424        let outer = SpineId(0, 3);
2425        let part_ids = [SpineId(0, 2), SpineId(1, 3)];
2426        let proto = rollup_proto_with_trace(|trace| {
2427            trace.spine_batches.push(ProtoIdSpineBatch {
2428                id: Some(outer.into_proto()),
2429                batch: Some(ProtoSpineBatch {
2430                    level: 0,
2431                    desc: Some(u64_desc_proto(0, 3, 0)),
2432                    parts: part_ids.iter().map(|id| id.into_proto()).collect(),
2433                    descs: vec![],
2434                }),
2435            });
2436            for id in part_ids {
2437                trace.hollow_batches.push(ProtoIdHollowBatch {
2438                    id: Some(id.into_proto()),
2439                    batch: Some(legacy_batch_proto(0, 1, 0)),
2440                });
2441            }
2442        });
2443        let err = rollup_decode_err(proto);
2444        assert!(err.contains("do not tile"), "{err}");
2445    }
2446
2447    /// Three spine batches at one level overflow the two-batch layer, which
2448    /// `MergeState::push_batch` previously `expect`ed against. It must be a
2449    /// decode error.
2450    #[mz_ore::test]
2451    fn rollup_proto_with_overfull_spine_level_is_rejected() {
2452        let proto = rollup_proto_with_trace(|trace| {
2453            for i in 0..3u64 {
2454                let id = SpineId(usize::cast_from(i), usize::cast_from(i + 1));
2455                trace.spine_batches.push(ProtoIdSpineBatch {
2456                    id: Some(id.into_proto()),
2457                    batch: Some(ProtoSpineBatch {
2458                        level: 0,
2459                        desc: Some(u64_desc_proto(i, i + 1, 0)),
2460                        parts: vec![id.into_proto()],
2461                        descs: vec![],
2462                    }),
2463                });
2464                trace.hollow_batches.push(ProtoIdHollowBatch {
2465                    id: Some(id.into_proto()),
2466                    batch: Some(legacy_batch_proto(i, i + 1, 0)),
2467                });
2468            }
2469        });
2470        let err = rollup_decode_err(proto);
2471        assert!(err.contains("full layer"), "{err}");
2472    }
2473
2474    #[mz_persist_proc::test(tokio::test)]
2475    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
2476    async fn state_diff_migration_rollups(dyncfgs: ConfigUpdates) {
2477        let r1_rollup = HollowRollup {
2478            key: PartialRollupKey("foo".to_owned()),
2479            encoded_size_bytes: None,
2480        };
2481        let r1 = StateFieldDiff {
2482            key: SeqNo(1),
2483            val: StateFieldValDiff::Insert(r1_rollup.clone()),
2484        };
2485        let r2_rollup = HollowRollup {
2486            key: PartialRollupKey("bar".to_owned()),
2487            encoded_size_bytes: Some(2),
2488        };
2489        let r2 = StateFieldDiff {
2490            key: SeqNo(2),
2491            val: StateFieldValDiff::Insert(r2_rollup.clone()),
2492        };
2493        let r3_rollup = HollowRollup {
2494            key: PartialRollupKey("baz".to_owned()),
2495            encoded_size_bytes: None,
2496        };
2497        let r3 = StateFieldDiff {
2498            key: SeqNo(3),
2499            val: StateFieldValDiff::Delete(r3_rollup.clone()),
2500        };
2501        let mut diff = StateDiff::<u64>::new(
2502            DUMMY_BUILD_INFO.semver_version(),
2503            SeqNo(4),
2504            SeqNo(5),
2505            0,
2506            PartialRollupKey("ignored".to_owned()),
2507        );
2508        diff.rollups.push(r2.clone());
2509        diff.rollups.push(r3.clone());
2510        let mut diff_proto = diff.into_proto();
2511
2512        let field_diffs = std::mem::take(&mut diff_proto.field_diffs).unwrap();
2513        let mut field_diffs_writer = field_diffs.into_writer();
2514
2515        // Manually add the old rollup encoding.
2516        field_diffs_into_proto(
2517            ProtoStateField::DeprecatedRollups,
2518            &[StateFieldDiff {
2519                key: r1.key,
2520                val: StateFieldValDiff::Insert(r1_rollup.key.clone()),
2521            }],
2522            &mut field_diffs_writer,
2523        );
2524
2525        assert_none!(diff_proto.field_diffs);
2526        diff_proto.field_diffs = Some(field_diffs_writer.into_proto());
2527
2528        let diff = StateDiff::<u64>::from_proto(diff_proto.clone()).unwrap();
2529        assert_eq!(
2530            diff.rollups.into_iter().collect::<Vec<_>>(),
2531            vec![r2, r3, r1]
2532        );
2533
2534        // Also make sure that a rollup delete in a diff applies cleanly to a
2535        // state that had it in the deprecated field.
2536        let shard_id = ShardId::new();
2537        let mut state = TypedState::<(), (), u64, i64>::new(
2538            DUMMY_BUILD_INFO.semver_version(),
2539            shard_id,
2540            "host".to_owned(),
2541            0,
2542        );
2543        state.state.seqno = SeqNo(4);
2544        let mut rollup = Rollup::from_untyped_state_without_diffs(state.into()).into_proto();
2545        rollup
2546            .deprecated_rollups
2547            .insert(3, r3_rollup.key.into_proto());
2548        let state: Rollup<u64> = rollup.into_rust().unwrap();
2549        let state = state.state;
2550        let mut state = state.check_codecs::<(), (), i64>(&shard_id).unwrap();
2551        let cache = new_test_client_cache(&dyncfgs);
2552        let encoded_diff = VersionedData {
2553            seqno: SeqNo(5),
2554            data: diff_proto.encode_to_vec().into(),
2555        };
2556        state.apply_encoded_diffs(cache.cfg(), &cache.metrics, std::iter::once(&encoded_diff));
2557        assert_eq!(
2558            state
2559                .state
2560                .collections
2561                .rollups
2562                .into_iter()
2563                .collect::<Vec<_>>(),
2564            vec![(SeqNo(1), r1_rollup), (SeqNo(2), r2_rollup)]
2565        );
2566    }
2567
2568    #[mz_ore::test]
2569    #[cfg_attr(miri, ignore)] // too slow
2570    fn state_proto_roundtrip() {
2571        fn testcase<T: Timestamp + Lattice + Codec64>(state: State<T>) {
2572            let before = UntypedState {
2573                key_codec: <() as Codec>::codec_name(),
2574                val_codec: <() as Codec>::codec_name(),
2575                ts_codec: <T as Codec64>::codec_name(),
2576                diff_codec: <i64 as Codec64>::codec_name(),
2577                state,
2578            };
2579            let proto = Rollup::from_untyped_state_without_diffs(before.clone()).into_proto();
2580            let after: Rollup<T> = proto.into_rust().unwrap();
2581            let after = after.state;
2582            assert_eq!(before, after);
2583        }
2584
2585        proptest!(|(state in any_state::<u64>(0..3))| testcase(state));
2586    }
2587
2588    #[mz_ore::test]
2589    fn check_data_versions() {
2590        #[track_caller]
2591        fn testcase(code: &str, data: &str, expected: Result<(), ()>) {
2592            let code = Version::parse(code).unwrap();
2593            let data = Version::parse(data).unwrap();
2594            #[allow(clippy::disallowed_methods)]
2595            let actual = cfg::code_can_write_data(&code, &data)
2596                .then_some(())
2597                .ok_or(());
2598            assert_eq!(actual, expected, "data at {data} read by code {code}");
2599        }
2600
2601        testcase("0.160.0-dev", "0.160.0-dev", Ok(()));
2602        testcase("0.160.0-dev", "0.160.0", Err(()));
2603        // Note: Probably useful to let tests use two arbitrary shas on main, at
2604        // the very least for things like git bisect.
2605        testcase("0.160.0-dev", "0.161.0-dev", Err(()));
2606        testcase("0.160.0-dev", "0.161.0", Err(()));
2607        testcase("0.160.0-dev", "0.162.0-dev", Err(()));
2608        testcase("0.160.0-dev", "0.162.0", Err(()));
2609        testcase("0.160.0-dev", "0.163.0-dev", Err(()));
2610
2611        testcase("0.160.0", "0.158.0-dev", Ok(()));
2612        testcase("0.160.0", "0.158.0", Ok(()));
2613        testcase("0.160.0", "0.159.0-dev", Ok(()));
2614        testcase("0.160.0", "0.159.0", Ok(()));
2615        testcase("0.160.0", "0.160.0-dev", Ok(()));
2616        testcase("0.160.0", "0.160.0", Ok(()));
2617
2618        testcase("0.160.0", "0.161.0-dev", Err(()));
2619        testcase("0.160.0", "0.161.0", Err(()));
2620        testcase("0.160.0", "0.161.1", Err(()));
2621        testcase("0.160.0", "0.161.1000000", Err(()));
2622        testcase("0.160.0", "0.162.0-dev", Err(()));
2623        testcase("0.160.0", "0.162.0", Err(()));
2624        testcase("0.160.0", "0.163.0-dev", Err(()));
2625
2626        testcase("0.160.1", "0.159.0", Ok(()));
2627        testcase("0.160.1", "0.160.0", Ok(()));
2628        testcase("0.160.1", "0.161.0", Err(()));
2629        testcase("0.160.1", "0.161.1", Err(()));
2630        testcase("0.160.1", "0.161.100", Err(()));
2631        testcase("0.160.0", "0.160.1", Err(()));
2632
2633        testcase("0.160.1", "26.0.0", Err(()));
2634        testcase("26.0.0", "0.160.1", Ok(()));
2635        testcase("26.2.0", "0.160.1", Ok(()));
2636        testcase("26.200.200", "0.160.1", Ok(()));
2637
2638        testcase("27.0.0", "0.160.1", Err(()));
2639        testcase("27.0.0", "0.16000.1", Err(()));
2640        testcase("27.0.0", "26.0.1", Ok(()));
2641        testcase("27.1000.100", "26.0.1", Ok(()));
2642        testcase("28.0.0", "26.0.1", Err(()));
2643        testcase("28.0.0", "26.1000.1", Err(()));
2644        testcase("28.0.0", "27.0.0", Ok(()));
2645    }
2646
2647    /// `LazyPartStats`'s bytes are stored undecoded, so `Debug` runs on
2648    /// encodings that came straight off blob and may be malformed. It must render
2649    /// the failure: `Spine::validate` formats the whole spine into the message
2650    /// with which `Trace::unflatten` rejects an untrusted rollup, so a panic here
2651    /// turns that rejection into an unrecoverable shard.
2652    #[mz_ore::test]
2653    fn lazy_part_stats_debug_does_not_panic_on_garbage() {
2654        // Tag 0 is never a legal protobuf tag.
2655        let stats = LazyPartStats::from_proto(Bytes::from_static(&[0x00, 0xff]))
2656            .expect("stats bytes are stored undecoded");
2657        assert_err!(stats.try_decode());
2658        assert!(format!("{stats:?}").contains("undecodable"));
2659    }
2660
2661    /// The exact shape version skew produces: valid protobuf whose stats
2662    /// oneof uses a variant this version does not know (a newer writer's new
2663    /// stats kind reaching an older reader).
2664    fn version_skewed_part_stats() -> LazyPartStats {
2665        let mut proto = ProtoStructStats::default();
2666        proto.cols.insert("c".into(), ProtoDynStats::default());
2667        let bytes = prost::Message::encode_to_vec(&proto);
2668        LazyPartStats::from_proto(Bytes::from(bytes)).expect("stats bytes are stored undecoded")
2669    }
2670
2671    /// Stats from a newer version are an error rather than a value this
2672    /// version misreads, which is what lets every read path fail open on
2673    /// them: the `shard_source` filter and the fast-path peek filter keep the
2674    /// part, the `stats()` accessors in fetch report `None`, `EXPLAIN FILTER
2675    /// PUSHDOWN` reports the part as selected, and inspect-state serializes
2676    /// the stats as absent.
2677    #[mz_ore::test]
2678    fn part_stats_try_decode_fails_open_on_unknown_variant() {
2679        assert_err!(version_skewed_part_stats().try_decode());
2680    }
2681}