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