Skip to main content

mnesis_store/
value.rs

1//! Validated value newtypes for envelope fields.
2//!
3//! Each newtype owns the wire-format invariants for one envelope field:
4//! UTF-8 validity (where applicable) and the size cap dictated by the
5//! wire format's length-prefix field. Once a value is constructed, it
6//! is by definition wire-encodable; downstream layers (`wire.rs`,
7//! adapters) skip re-validation.
8//!
9//! Backed by `bytes::Bytes` for cheap Arc-shared ownership. The
10//! `Bytes::from_static` path makes literal event-type names
11//! allocation-free, matching the previous `&'static str` ergonomics.
12
13use core::num::NonZeroU32;
14
15use bytes::Bytes;
16use thiserror::Error;
17
18/// Maximum event-type length (the wire format reserves a `u16` length field).
19#[allow(
20    clippy::as_conversions,
21    reason = "const-context u16→usize widening; lossless on all targets"
22)]
23pub const MAX_EVENT_TYPE_LEN: usize = u16::MAX as usize;
24
25/// Maximum metadata length. One less than `u32::MAX` because the wire
26/// format uses `u32::MAX` as the absent-metadata sentinel.
27#[allow(
28    clippy::as_conversions,
29    reason = "const-context u32→usize widening; lossless on 32+ bit targets"
30)]
31pub const MAX_METADATA_LEN: usize = (u32::MAX - 1) as usize;
32
33/// Maximum payload length (the wire format reserves a `u32` length field).
34#[allow(
35    clippy::as_conversions,
36    reason = "const-context u32→usize widening; lossless on 32+ bit targets"
37)]
38pub const MAX_PAYLOAD_LEN: usize = u32::MAX as usize;
39
40/// Construction errors for value newtypes.
41#[derive(Debug, Error)]
42#[non_exhaustive]
43pub enum ValueError {
44    #[error("event_type length {actual} exceeds maximum {MAX_EVENT_TYPE_LEN}")]
45    EventTypeTooLong { actual: usize },
46    #[error("invalid UTF-8 in event_type bytes (at byte {valid_up_to})")]
47    EventTypeInvalidUtf8 {
48        valid_up_to: usize,
49        #[source]
50        source: core::str::Utf8Error,
51    },
52    #[error("payload length {actual} exceeds maximum {MAX_PAYLOAD_LEN}")]
53    PayloadTooLong { actual: usize },
54    #[error("metadata length {actual} exceeds maximum {MAX_METADATA_LEN}")]
55    MetadataTooLong { actual: usize },
56    #[error("metadata is empty; use Option::None to represent absent metadata")]
57    MetadataEmpty,
58    #[error("schema_version must be > 0 (got 0)")]
59    SchemaVersionZero,
60}
61
62/// A validated event type name.
63///
64/// Invariants: valid UTF-8, length ≤ [`MAX_EVENT_TYPE_LEN`].
65///
66/// Backed by [`Bytes`]; constructible from a `&'static str` literal at
67/// zero allocation via [`from_static_str`](Self::from_static_str), or
68/// from arbitrary [`Bytes`] via the validating
69/// [`from_bytes`](Self::from_bytes).
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub struct EventType {
72    inner: Bytes,
73}
74
75impl EventType {
76    /// Construct from a `&'static str` literal. Infallible for valid
77    /// literals: `&str` is UTF-8 by Rust's type system, and program-text
78    /// literals are bounded by the source file.
79    ///
80    /// # Panics
81    ///
82    /// Panics if `s.len() > MAX_EVENT_TYPE_LEN`. For string literals this
83    /// is a programmer error caught at first use; the runtime check
84    /// catches the `Box::leak(String::from(runtime))` case that would
85    /// otherwise silently corrupt the wire encoding.
86    #[must_use]
87    pub fn from_static_str(s: &'static str) -> Self {
88        assert!(
89            s.len() <= MAX_EVENT_TYPE_LEN,
90            "event_type length {} exceeds MAX_EVENT_TYPE_LEN ({})",
91            s.len(),
92            MAX_EVENT_TYPE_LEN,
93        );
94        Self {
95            inner: Bytes::from_static(s.as_bytes()),
96        }
97    }
98
99    /// Construct from arbitrary bytes, validating UTF-8 and size cap.
100    ///
101    /// # Errors
102    ///
103    /// - [`ValueError::EventTypeTooLong`] if `bytes.len() > MAX_EVENT_TYPE_LEN`.
104    /// - [`ValueError::EventTypeInvalidUtf8`] if `bytes` is not valid UTF-8.
105    pub fn from_bytes(bytes: Bytes) -> Result<Self, ValueError> {
106        if bytes.len() > MAX_EVENT_TYPE_LEN {
107            return Err(ValueError::EventTypeTooLong {
108                actual: bytes.len(),
109            });
110        }
111        core::str::from_utf8(&bytes).map_err(|e| ValueError::EventTypeInvalidUtf8 {
112            valid_up_to: e.valid_up_to(),
113            source: e,
114        })?;
115        Ok(Self { inner: bytes })
116    }
117
118    /// Construct from already-validated bytes — crate-internal fast path.
119    ///
120    /// # Safety
121    ///
122    /// The caller MUST guarantee both invariants:
123    /// - `bytes` is valid UTF-8.
124    /// - `bytes.len() <= MAX_EVENT_TYPE_LEN`.
125    ///
126    /// Violating either invariant makes [`EventType::as_str`] undefined
127    /// behavior in release builds. The `debug_assert!`s below are
128    /// diagnostic-only and compiled out in release.
129    ///
130    /// The read path ([`crate::envelope::PersistedEnvelope::event_type_value`])
131    /// uses this after `try_new` has already validated.
132    pub(crate) unsafe fn from_validated_bytes(bytes: Bytes) -> Self {
133        debug_assert!(
134            bytes.len() <= MAX_EVENT_TYPE_LEN,
135            "from_validated_bytes invariant: length ≤ MAX_EVENT_TYPE_LEN"
136        );
137        debug_assert!(
138            core::str::from_utf8(&bytes).is_ok(),
139            "from_validated_bytes invariant: valid UTF-8"
140        );
141        Self { inner: bytes }
142    }
143
144    /// Borrow as `&str`. Zero-cost; UTF-8 is guaranteed by construction.
145    #[must_use]
146    pub fn as_str(&self) -> &str {
147        // SAFETY: every constructor validates UTF-8 (or accepts
148        // `&'static str` which is UTF-8 by Rust's type system).
149        #[allow(
150            unsafe_code,
151            reason = "UTF-8 invariant established by every constructor"
152        )]
153        unsafe {
154            core::str::from_utf8_unchecked(&self.inner)
155        }
156    }
157
158    /// Borrow as `&[u8]`.
159    #[must_use]
160    pub fn as_bytes(&self) -> &[u8] {
161        &self.inner
162    }
163
164    /// Take ownership of the inner [`Bytes`] (one Arc share, no copy).
165    #[must_use]
166    pub fn into_bytes(self) -> Bytes {
167        self.inner
168    }
169}
170
171/// A validated event payload.
172///
173/// Invariant: length ≤ [`MAX_PAYLOAD_LEN`].
174///
175/// Backed by [`Bytes`] for Arc-shared ownership. Empty payloads are
176/// accepted — unlike [`Metadata`], an empty payload is a legal event
177/// shape (e.g., a marker event with no data).
178#[derive(Debug, Clone, PartialEq, Eq, Hash)]
179pub struct Payload {
180    inner: Bytes,
181}
182
183impl Payload {
184    /// Construct from arbitrary bytes, validating the size cap.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`ValueError::PayloadTooLong`] if `bytes.len() > MAX_PAYLOAD_LEN`.
189    pub fn from_bytes(bytes: Bytes) -> Result<Self, ValueError> {
190        if bytes.len() > MAX_PAYLOAD_LEN {
191            return Err(ValueError::PayloadTooLong {
192                actual: bytes.len(),
193            });
194        }
195        Ok(Self { inner: bytes })
196    }
197
198    /// Construct from already-validated bytes — crate-internal fast path.
199    ///
200    /// # Safety
201    ///
202    /// The caller MUST guarantee `bytes.len() <= MAX_PAYLOAD_LEN`.
203    ///
204    /// Violating the invariant does not produce undefined behavior in this
205    /// type's own methods (no `unsafe` consumers), but it will corrupt the
206    /// wire encoding downstream when [`crate::wire::encode_frame`] tries
207    /// to fit the length into a `u32` field. Marked `unsafe` for
208    /// consistency with the other `from_validated_bytes` constructors in
209    /// this module and to make the invariant obligation visible at call
210    /// sites.
211    ///
212    /// The read path ([`crate::envelope::PersistedEnvelope::payload_value`])
213    /// uses this after the buffer's length has been implicitly capped by
214    /// the wire format's `u32` length field.
215    pub(crate) unsafe fn from_validated_bytes(bytes: Bytes) -> Self {
216        debug_assert!(
217            bytes.len() <= MAX_PAYLOAD_LEN,
218            "from_validated_bytes invariant: length ≤ MAX_PAYLOAD_LEN"
219        );
220        Self { inner: bytes }
221    }
222
223    /// Borrow as `&[u8]`. Zero-cost.
224    #[must_use]
225    pub fn as_slice(&self) -> &[u8] {
226        &self.inner
227    }
228
229    /// Take ownership of the inner [`Bytes`] (one Arc share, no copy).
230    #[must_use]
231    pub fn into_bytes(self) -> Bytes {
232        self.inner
233    }
234}
235
236/// Validated envelope metadata.
237///
238/// Invariants:
239/// - Length is in `1..=MAX_METADATA_LEN`. Empty metadata is rejected —
240///   use `Option::<Metadata>::None` to represent "absent." This avoids
241///   the `bytes::Bytes::slice(empty)` footgun where an empty slice
242///   orphans from the parent buffer's `STATIC_VTABLE` (the footgun is
243///   documented on [`crate::envelope::PersistedEnvelope::metadata_bytes`]).
244/// - The non-empty invariant is wire-format-driven: `u32::MAX` is the
245///   absent-metadata sentinel in the wire encoding, so `Some(Metadata)`
246///   on the read path always carries actual bytes.
247///
248/// Backed by [`Bytes`] for Arc-shared ownership.
249#[derive(Debug, Clone, PartialEq, Eq, Hash)]
250pub struct Metadata {
251    inner: Bytes,
252}
253
254impl Metadata {
255    /// Construct from arbitrary bytes, validating non-empty and size cap.
256    ///
257    /// # Errors
258    ///
259    /// - [`ValueError::MetadataEmpty`] if `bytes.is_empty()`.
260    /// - [`ValueError::MetadataTooLong`] if `bytes.len() > MAX_METADATA_LEN`.
261    pub fn from_bytes(bytes: Bytes) -> Result<Self, ValueError> {
262        if bytes.is_empty() {
263            return Err(ValueError::MetadataEmpty);
264        }
265        if bytes.len() > MAX_METADATA_LEN {
266            return Err(ValueError::MetadataTooLong {
267                actual: bytes.len(),
268            });
269        }
270        Ok(Self { inner: bytes })
271    }
272
273    /// Construct from already-validated bytes — crate-internal fast path.
274    ///
275    /// # Safety
276    ///
277    /// The caller MUST guarantee:
278    /// - `!bytes.is_empty()` (the wire format's `u32::MAX` sentinel handles
279    ///   absent metadata; a `Some(Metadata)` must carry actual bytes).
280    /// - `bytes.len() <= MAX_METADATA_LEN`.
281    ///
282    /// Violating these invariants does not produce undefined behavior in
283    /// this type's own methods (no `unsafe` consumers), but it will
284    /// surface as `WireError::FrameLengthOverflow` at encode time or
285    /// trigger the `bytes::Bytes::slice(empty)` `STATIC_VTABLE` footgun
286    /// on the read path. Marked `unsafe` for consistency with the other
287    /// `from_validated_bytes` constructors and to make the invariant
288    /// obligation visible at call sites.
289    ///
290    /// The read path ([`crate::envelope::PersistedEnvelope::metadata_value`])
291    /// uses this after the wire decoder has rejected the absent sentinel
292    /// into `Option::None`.
293    pub(crate) unsafe fn from_validated_bytes(bytes: Bytes) -> Self {
294        debug_assert!(
295            !bytes.is_empty(),
296            "from_validated_bytes invariant: non-empty"
297        );
298        debug_assert!(
299            bytes.len() <= MAX_METADATA_LEN,
300            "from_validated_bytes invariant: length ≤ MAX_METADATA_LEN"
301        );
302        Self { inner: bytes }
303    }
304
305    /// Borrow as `&[u8]`. Zero-cost.
306    #[must_use]
307    pub fn as_slice(&self) -> &[u8] {
308        &self.inner
309    }
310
311    /// Take ownership of the inner [`Bytes`] (one Arc share, no copy).
312    #[must_use]
313    pub fn into_bytes(self) -> Bytes {
314        self.inner
315    }
316}
317
318/// Validated schema version — the wire-format-sized sibling of the kernel's
319/// [`mnesis::Version`].
320///
321/// Invariant: nonzero, fits in `u32` (the wire format's schema-version field).
322///
323/// The kernel's `Version` is `NonZeroU64` because aggregate streams can
324/// in principle be unboundedly long; `SchemaVersion` is `NonZeroU32`
325/// because the wire format chose that width. Same invariant, different
326/// width — explicit total conversion to `Version` is provided for the
327/// upcaster path.
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
329pub struct SchemaVersion(NonZeroU32);
330
331impl SchemaVersion {
332    /// The initial schema version (`1`).
333    pub const INITIAL: Self = Self(NonZeroU32::MIN);
334
335    /// Construct from a `NonZeroU32`. Infallible.
336    #[must_use]
337    pub const fn new(value: NonZeroU32) -> Self {
338        Self(value)
339    }
340
341    /// Construct from a raw `u32`, validating the nonzero invariant.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`ValueError::SchemaVersionZero`] if `value == 0`.
346    pub fn from_u32(value: u32) -> Result<Self, ValueError> {
347        NonZeroU32::new(value)
348            .map(Self)
349            .ok_or(ValueError::SchemaVersionZero)
350    }
351
352    /// The inner `u32` value (always > 0).
353    #[must_use]
354    pub const fn get(self) -> u32 {
355        self.0.get()
356    }
357}
358
359impl From<SchemaVersion> for mnesis::Version {
360    /// Widen to the kernel's `Version` (`NonZeroU64`). Total — `NonZeroU32`
361    /// always fits in `NonZeroU64`.
362    #[allow(
363        clippy::expect_used,
364        reason = "NonZeroU32 widened to u64 is structurally nonzero; the None arm of Version::new is unreachable"
365    )]
366    fn from(sv: SchemaVersion) -> Self {
367        Self::new(u64::from(sv.get())).expect("NonZeroU32 widened to u64 is nonzero")
368    }
369}
370
371#[cfg(test)]
372mod event_type_tests {
373    use super::*;
374    use bytes::Bytes;
375
376    #[test]
377    fn from_static_str_accepts_literal() {
378        let et = EventType::from_static_str("UserCreated");
379        assert_eq!(et.as_str(), "UserCreated");
380    }
381
382    #[test]
383    fn from_bytes_accepts_valid_utf8_within_cap() {
384        let et = EventType::from_bytes(Bytes::from_static(b"OrderPlaced"))
385            .expect("valid utf-8 within cap");
386        assert_eq!(et.as_str(), "OrderPlaced");
387    }
388
389    #[test]
390    fn from_bytes_rejects_invalid_utf8() {
391        let err = EventType::from_bytes(Bytes::from_static(&[0xFFu8, 0xFE]))
392            .expect_err("must reject invalid utf-8");
393        assert!(matches!(err, ValueError::EventTypeInvalidUtf8 { .. }));
394    }
395
396    #[test]
397    fn from_bytes_rejects_oversize() {
398        let too_big = Bytes::from(vec![b'a'; MAX_EVENT_TYPE_LEN + 1]);
399        let err =
400            EventType::from_bytes(too_big).expect_err("must reject length > MAX_EVENT_TYPE_LEN");
401        assert!(
402            matches!(err, ValueError::EventTypeTooLong { actual } if actual == MAX_EVENT_TYPE_LEN + 1)
403        );
404    }
405
406    #[test]
407    fn at_max_cap_accepted() {
408        let at_cap = Bytes::from(vec![b'a'; MAX_EVENT_TYPE_LEN]);
409        let et = EventType::from_bytes(at_cap).expect("length at cap is valid");
410        assert_eq!(et.as_bytes().len(), MAX_EVENT_TYPE_LEN);
411    }
412
413    #[test]
414    fn into_bytes_returns_inner_arc() {
415        let et = EventType::from_static_str("Foo");
416        let bytes = et.into_bytes();
417        assert_eq!(bytes.as_ref(), b"Foo");
418    }
419
420    #[test]
421    fn debug_redacts_nothing_short_names_show_in_full() {
422        let et = EventType::from_static_str("Foo");
423        let dbg = format!("{et:?}");
424        assert!(dbg.contains("Foo"), "Debug should include event type name");
425        assert!(
426            dbg.len() < 40,
427            "Debug should be tight (no padding/extra fields): got {} chars: {dbg}",
428            dbg.len(),
429        );
430    }
431
432    #[test]
433    #[should_panic(expected = "exceeds MAX_EVENT_TYPE_LEN")]
434    fn from_static_str_panics_on_leaked_oversize_string() {
435        // Box::leak of a runtime-built String over MAX_EVENT_TYPE_LEN is the
436        // pathological case the runtime assert! catches. A future refactor
437        // that turns the assert! into debug_assert! (compiled out in release)
438        // would regress this guard silently — this test makes that regression
439        // a hard error.
440        let big = "a".repeat(MAX_EVENT_TYPE_LEN + 1);
441        let leaked: &'static str = Box::leak(big.into_boxed_str());
442        let _ = EventType::from_static_str(leaked);
443    }
444}
445
446#[cfg(test)]
447mod payload_tests {
448    use super::*;
449
450    #[test]
451    fn from_bytes_accepts_empty() {
452        let p = Payload::from_bytes(Bytes::new()).expect("empty payload is valid");
453        assert!(p.as_slice().is_empty());
454    }
455
456    #[test]
457    fn from_bytes_accepts_small() {
458        let p = Payload::from_bytes(Bytes::from_static(b"hello")).expect("valid");
459        assert_eq!(p.as_slice(), b"hello");
460    }
461
462    #[test]
463    fn into_bytes_returns_inner_arc() {
464        let p = Payload::from_bytes(Bytes::from_static(b"payload")).expect("valid");
465        let bytes = p.into_bytes();
466        assert_eq!(bytes.as_ref(), b"payload");
467    }
468
469    // Note: oversize check (length > MAX_PAYLOAD_LEN = u32::MAX) is
470    // covered by the property tests in Task 1.6 with a synthetic length
471    // boundary. Allocating ~4 GB in a unit test is impractical.
472}
473
474#[cfg(test)]
475mod metadata_tests {
476    use super::*;
477
478    #[test]
479    fn from_bytes_accepts_small() {
480        let m = Metadata::from_bytes(Bytes::from_static(b"meta")).expect("valid");
481        assert_eq!(m.as_slice(), b"meta");
482    }
483
484    #[test]
485    fn from_bytes_rejects_empty() {
486        let err = Metadata::from_bytes(Bytes::new())
487            .expect_err("empty metadata must be rejected — use Option::None for absent");
488        assert!(matches!(err, ValueError::MetadataEmpty));
489    }
490
491    #[test]
492    fn into_bytes_returns_inner_arc() {
493        let m = Metadata::from_bytes(Bytes::from_static(b"m")).expect("valid");
494        let bytes = m.into_bytes();
495        assert_eq!(bytes.as_ref(), b"m");
496    }
497}
498
499#[cfg(test)]
500mod schema_version_tests {
501    use super::*;
502    use std::num::NonZeroU32;
503
504    #[test]
505    fn new_accepts_nonzero() {
506        let nz = NonZeroU32::new(1).expect("nonzero");
507        let sv = SchemaVersion::new(nz);
508        assert_eq!(sv.get(), 1);
509    }
510
511    #[test]
512    fn initial_is_one() {
513        assert_eq!(SchemaVersion::INITIAL.get(), 1);
514    }
515
516    #[test]
517    fn from_u32_accepts_nonzero() {
518        let sv = SchemaVersion::from_u32(42).expect("nonzero");
519        assert_eq!(sv.get(), 42);
520    }
521
522    #[test]
523    fn from_u32_rejects_zero() {
524        let err = SchemaVersion::from_u32(0).expect_err("zero rejected");
525        assert!(matches!(err, ValueError::SchemaVersionZero));
526    }
527
528    #[test]
529    fn into_version_widens_to_nonzero_u64() {
530        let sv = SchemaVersion::from_u32(7).expect("nonzero");
531        let v: mnesis::Version = sv.into();
532        assert_eq!(v.as_u64(), 7);
533    }
534}
535
536#[cfg(test)]
537mod property_tests {
538    use super::*;
539    use bytes::Bytes;
540    use proptest::prelude::*;
541    use std::num::NonZeroU32;
542
543    proptest! {
544        #[test]
545        fn event_type_from_bytes_roundtrips_valid_utf8(s in "[a-zA-Z][a-zA-Z0-9_]{0,63}") {
546            let bytes = Bytes::from(s.clone().into_bytes());
547            let et = EventType::from_bytes(bytes).expect("valid");
548            prop_assert_eq!(et.as_str(), &s);
549        }
550
551        #[test]
552        fn payload_arbitrary_bytes_within_cap_accepted(
553            buf in proptest::collection::vec(any::<u8>(), 0..4096),
554        ) {
555            let p = Payload::from_bytes(Bytes::from(buf.clone())).expect("under cap");
556            prop_assert_eq!(p.as_slice(), buf.as_slice());
557        }
558
559        #[test]
560        fn metadata_nonempty_within_cap_accepted(
561            buf in proptest::collection::vec(any::<u8>(), 1..4096),
562        ) {
563            let m = Metadata::from_bytes(Bytes::from(buf.clone())).expect("nonempty, under cap");
564            prop_assert_eq!(m.as_slice(), buf.as_slice());
565        }
566
567        #[test]
568        fn event_type_invalid_utf8_always_rejected(
569            bytes in proptest::collection::vec(any::<u8>(), 1..256),
570        ) {
571            // Filter to bytes containing 0xFE or 0xFF, which are never valid
572            // UTF-8 start bytes — guarantees the input is invalid UTF-8.
573            prop_assume!(bytes.iter().any(|&b| b == 0xFEu8 || b == 0xFFu8));
574            let result = EventType::from_bytes(Bytes::from(bytes));
575            prop_assert!(result.is_err());
576        }
577
578        #[test]
579        fn schema_version_from_u32_roundtrips_nonzero(value in 1u32..) {
580            let sv = SchemaVersion::from_u32(value).expect("nonzero");
581            prop_assert_eq!(sv.get(), value);
582        }
583
584        #[test]
585        fn schema_version_widens_to_version(value in 1u32..) {
586            let sv = SchemaVersion::from_u32(value).expect("nonzero");
587            let v: mnesis::Version = sv.into();
588            prop_assert_eq!(v.as_u64(), u64::from(value));
589        }
590
591        #[test]
592        fn schema_version_ord_agrees_with_inner_u32_ord(
593            a in 1u32..,
594            b in 1u32..,
595        ) {
596            let sa = SchemaVersion::from_u32(a).expect("nonzero");
597            let sb = SchemaVersion::from_u32(b).expect("nonzero");
598            prop_assert_eq!(sa.cmp(&sb), a.cmp(&b));
599        }
600    }
601
602    // Pure unit tests for the structural boundaries that proptest can't
603    // exercise cheaply (4 GiB+ allocations).
604    #[test]
605    fn metadata_empty_always_rejected() {
606        let err = Metadata::from_bytes(Bytes::new()).expect_err("empty metadata always rejected");
607        assert!(matches!(err, ValueError::MetadataEmpty));
608    }
609
610    #[test]
611    fn schema_version_zero_rejected() {
612        let err = SchemaVersion::from_u32(0).expect_err("zero rejected");
613        assert!(matches!(err, ValueError::SchemaVersionZero));
614    }
615
616    #[test]
617    fn schema_version_initial_equals_one() {
618        assert_eq!(SchemaVersion::INITIAL.get(), 1);
619    }
620
621    #[test]
622    fn schema_version_initial_widens_to_version_one() {
623        let v: mnesis::Version = SchemaVersion::INITIAL.into();
624        assert_eq!(v.as_u64(), 1);
625    }
626
627    #[test]
628    fn schema_version_new_from_nonzero_construction() {
629        let nz = NonZeroU32::new(99).expect("nonzero");
630        let sv = SchemaVersion::new(nz);
631        assert_eq!(sv.get(), 99);
632    }
633}