Skip to main content

mz_persist_client/internal/
encoding.rs

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