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    /// Not `unsafe`: violating the `bytes.len() <= MAX_PAYLOAD_LEN` precondition
201    /// produces **no undefined behavior** in this type (there are no `unsafe`
202    /// consumers of a `Payload`) — worst case is a downstream, *typed*
203    /// [`crate::wire::WireError`] when [`crate::wire::encode_frame`] cannot fit
204    /// the length into its `u32` field. An `unsafe fn` with no UB contract only
205    /// dilutes the keyword and forces callers into false-assurance `unsafe {}`
206    /// blocks; the `debug_assert` below still flags a caller bug in tests.
207    ///
208    /// The read path ([`crate::envelope::PersistedEnvelope::payload_value`])
209    /// uses this after the buffer's length has been implicitly capped by the
210    /// wire format's `u32` length field.
211    pub(crate) fn from_validated_bytes(bytes: Bytes) -> Self {
212        debug_assert!(
213            bytes.len() <= MAX_PAYLOAD_LEN,
214            "from_validated_bytes invariant: length ≤ MAX_PAYLOAD_LEN"
215        );
216        Self { inner: bytes }
217    }
218
219    /// Borrow as `&[u8]`. Zero-cost.
220    #[must_use]
221    pub fn as_slice(&self) -> &[u8] {
222        &self.inner
223    }
224
225    /// Take ownership of the inner [`Bytes`] (one Arc share, no copy).
226    #[must_use]
227    pub fn into_bytes(self) -> Bytes {
228        self.inner
229    }
230}
231
232/// Validated envelope metadata.
233///
234/// Invariants:
235/// - Length is in `1..=MAX_METADATA_LEN`. Empty metadata is rejected —
236///   use `Option::<Metadata>::None` to represent "absent." This avoids
237///   the `bytes::Bytes::slice(empty)` footgun where an empty slice
238///   orphans from the parent buffer's `STATIC_VTABLE` (the footgun is
239///   documented on [`crate::envelope::PersistedEnvelope::metadata_bytes`]).
240/// - The non-empty invariant is wire-format-driven: `u32::MAX` is the
241///   absent-metadata sentinel in the wire encoding, so `Some(Metadata)`
242///   on the read path always carries actual bytes.
243///
244/// Backed by [`Bytes`] for Arc-shared ownership.
245#[derive(Debug, Clone, PartialEq, Eq, Hash)]
246pub struct Metadata {
247    inner: Bytes,
248}
249
250impl Metadata {
251    /// Construct from arbitrary bytes, validating non-empty and size cap.
252    ///
253    /// # Errors
254    ///
255    /// - [`ValueError::MetadataEmpty`] if `bytes.is_empty()`.
256    /// - [`ValueError::MetadataTooLong`] if `bytes.len() > MAX_METADATA_LEN`.
257    pub fn from_bytes(bytes: Bytes) -> Result<Self, ValueError> {
258        if bytes.is_empty() {
259            return Err(ValueError::MetadataEmpty);
260        }
261        if bytes.len() > MAX_METADATA_LEN {
262            return Err(ValueError::MetadataTooLong {
263                actual: bytes.len(),
264            });
265        }
266        Ok(Self { inner: bytes })
267    }
268
269    /// Construct from already-validated bytes — crate-internal fast path.
270    ///
271    /// Not `unsafe`: violating the preconditions produces **no undefined
272    /// behavior** in this type (there are no `unsafe` consumers of a
273    /// `Metadata`). The caller should uphold:
274    /// - `!bytes.is_empty()` (the wire format's `u32::MAX` sentinel handles
275    ///   absent metadata; a `Some(Metadata)` must carry actual bytes).
276    /// - `bytes.len() <= MAX_METADATA_LEN`.
277    ///
278    /// Violating them surfaces only as a downstream, *typed* failure — a
279    /// `WireError::FrameLengthOverflow` at encode time, or the
280    /// `bytes::Bytes::slice(empty)` `STATIC_VTABLE` footgun on the read path —
281    /// never UB, so `unsafe` would only dilute the keyword. The `debug_assert`s
282    /// below still flag a caller bug in tests.
283    ///
284    /// The read path ([`crate::envelope::PersistedEnvelope::metadata_value`])
285    /// uses this after the wire decoder has rejected the absent sentinel
286    /// into `Option::None`.
287    pub(crate) fn from_validated_bytes(bytes: Bytes) -> Self {
288        debug_assert!(
289            !bytes.is_empty(),
290            "from_validated_bytes invariant: non-empty"
291        );
292        debug_assert!(
293            bytes.len() <= MAX_METADATA_LEN,
294            "from_validated_bytes invariant: length ≤ MAX_METADATA_LEN"
295        );
296        Self { inner: bytes }
297    }
298
299    /// Borrow as `&[u8]`. Zero-cost.
300    #[must_use]
301    pub fn as_slice(&self) -> &[u8] {
302        &self.inner
303    }
304
305    /// Take ownership of the inner [`Bytes`] (one Arc share, no copy).
306    #[must_use]
307    pub fn into_bytes(self) -> Bytes {
308        self.inner
309    }
310}
311
312/// Validated schema version — the wire-format-sized sibling of the kernel's
313/// [`mnesis::Version`].
314///
315/// Invariant: nonzero, fits in `u32` (the wire format's schema-version field).
316///
317/// The kernel's `Version` is `NonZeroU64` because aggregate streams can
318/// in principle be unboundedly long; `SchemaVersion` is `NonZeroU32`
319/// because the wire format chose that width. Same invariant, different
320/// width — explicit total conversion to `Version` is provided for the
321/// upcaster path.
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
323pub struct SchemaVersion(NonZeroU32);
324
325impl SchemaVersion {
326    /// The initial schema version (`1`).
327    pub const INITIAL: Self = Self(NonZeroU32::MIN);
328
329    /// Construct from a `NonZeroU32`. Infallible.
330    #[must_use]
331    pub const fn new(value: NonZeroU32) -> Self {
332        Self(value)
333    }
334
335    /// Construct from a raw `u32`, validating the nonzero invariant.
336    ///
337    /// # Errors
338    ///
339    /// Returns [`ValueError::SchemaVersionZero`] if `value == 0`.
340    pub fn from_u32(value: u32) -> Result<Self, ValueError> {
341        NonZeroU32::new(value)
342            .map(Self)
343            .ok_or(ValueError::SchemaVersionZero)
344    }
345
346    /// The inner `u32` value (always > 0).
347    #[must_use]
348    pub const fn get(self) -> u32 {
349        self.0.get()
350    }
351}
352
353impl From<SchemaVersion> for mnesis::Version {
354    /// Widen to the kernel's `Version` (`NonZeroU64`). Total — `NonZeroU32`
355    /// always fits in `NonZeroU64`.
356    #[allow(
357        clippy::expect_used,
358        reason = "NonZeroU32 widened to u64 is structurally nonzero; the None arm of Version::new is unreachable"
359    )]
360    fn from(sv: SchemaVersion) -> Self {
361        Self::new(u64::from(sv.get())).expect("NonZeroU32 widened to u64 is nonzero")
362    }
363}
364
365#[cfg(test)]
366mod event_type_tests {
367    use super::*;
368    use bytes::Bytes;
369
370    #[test]
371    fn from_static_str_accepts_literal() {
372        let et = EventType::from_static_str("UserCreated");
373        assert_eq!(et.as_str(), "UserCreated");
374    }
375
376    #[test]
377    fn from_bytes_accepts_valid_utf8_within_cap() {
378        let et = EventType::from_bytes(Bytes::from_static(b"OrderPlaced"))
379            .expect("valid utf-8 within cap");
380        assert_eq!(et.as_str(), "OrderPlaced");
381    }
382
383    #[test]
384    fn from_bytes_rejects_invalid_utf8() {
385        let err = EventType::from_bytes(Bytes::from_static(&[0xFFu8, 0xFE]))
386            .expect_err("must reject invalid utf-8");
387        assert!(matches!(err, ValueError::EventTypeInvalidUtf8 { .. }));
388    }
389
390    #[test]
391    fn from_bytes_rejects_oversize() {
392        let too_big = Bytes::from(vec![b'a'; MAX_EVENT_TYPE_LEN + 1]);
393        let err =
394            EventType::from_bytes(too_big).expect_err("must reject length > MAX_EVENT_TYPE_LEN");
395        assert!(
396            matches!(err, ValueError::EventTypeTooLong { actual } if actual == MAX_EVENT_TYPE_LEN + 1)
397        );
398    }
399
400    #[test]
401    fn at_max_cap_accepted() {
402        let at_cap = Bytes::from(vec![b'a'; MAX_EVENT_TYPE_LEN]);
403        let et = EventType::from_bytes(at_cap).expect("length at cap is valid");
404        assert_eq!(et.as_bytes().len(), MAX_EVENT_TYPE_LEN);
405    }
406
407    #[test]
408    fn into_bytes_returns_inner_arc() {
409        let et = EventType::from_static_str("Foo");
410        let bytes = et.into_bytes();
411        assert_eq!(bytes.as_ref(), b"Foo");
412    }
413
414    #[test]
415    fn debug_redacts_nothing_short_names_show_in_full() {
416        let et = EventType::from_static_str("Foo");
417        let dbg = format!("{et:?}");
418        assert!(dbg.contains("Foo"), "Debug should include event type name");
419        assert!(
420            dbg.len() < 40,
421            "Debug should be tight (no padding/extra fields): got {} chars: {dbg}",
422            dbg.len(),
423        );
424    }
425
426    #[test]
427    #[should_panic(expected = "exceeds MAX_EVENT_TYPE_LEN")]
428    fn from_static_str_panics_on_leaked_oversize_string() {
429        // Box::leak of a runtime-built String over MAX_EVENT_TYPE_LEN is the
430        // pathological case the runtime assert! catches. A future refactor
431        // that turns the assert! into debug_assert! (compiled out in release)
432        // would regress this guard silently — this test makes that regression
433        // a hard error.
434        let big = "a".repeat(MAX_EVENT_TYPE_LEN + 1);
435        let leaked: &'static str = Box::leak(big.into_boxed_str());
436        let _ = EventType::from_static_str(leaked);
437    }
438}
439
440#[cfg(test)]
441mod payload_tests {
442    use super::*;
443
444    #[test]
445    fn from_bytes_accepts_empty() {
446        let p = Payload::from_bytes(Bytes::new()).expect("empty payload is valid");
447        assert!(p.as_slice().is_empty());
448    }
449
450    #[test]
451    fn from_bytes_accepts_small() {
452        let p = Payload::from_bytes(Bytes::from_static(b"hello")).expect("valid");
453        assert_eq!(p.as_slice(), b"hello");
454    }
455
456    #[test]
457    fn into_bytes_returns_inner_arc() {
458        let p = Payload::from_bytes(Bytes::from_static(b"payload")).expect("valid");
459        let bytes = p.into_bytes();
460        assert_eq!(bytes.as_ref(), b"payload");
461    }
462
463    // Note: oversize check (length > MAX_PAYLOAD_LEN = u32::MAX) is
464    // covered by the property tests in Task 1.6 with a synthetic length
465    // boundary. Allocating ~4 GB in a unit test is impractical.
466}
467
468#[cfg(test)]
469mod metadata_tests {
470    use super::*;
471
472    #[test]
473    fn from_bytes_accepts_small() {
474        let m = Metadata::from_bytes(Bytes::from_static(b"meta")).expect("valid");
475        assert_eq!(m.as_slice(), b"meta");
476    }
477
478    #[test]
479    fn from_bytes_rejects_empty() {
480        let err = Metadata::from_bytes(Bytes::new())
481            .expect_err("empty metadata must be rejected — use Option::None for absent");
482        assert!(matches!(err, ValueError::MetadataEmpty));
483    }
484
485    #[test]
486    fn into_bytes_returns_inner_arc() {
487        let m = Metadata::from_bytes(Bytes::from_static(b"m")).expect("valid");
488        let bytes = m.into_bytes();
489        assert_eq!(bytes.as_ref(), b"m");
490    }
491}
492
493#[cfg(test)]
494mod schema_version_tests {
495    use super::*;
496    use std::num::NonZeroU32;
497
498    #[test]
499    fn new_accepts_nonzero() {
500        let nz = NonZeroU32::new(1).expect("nonzero");
501        let sv = SchemaVersion::new(nz);
502        assert_eq!(sv.get(), 1);
503    }
504
505    #[test]
506    fn initial_is_one() {
507        assert_eq!(SchemaVersion::INITIAL.get(), 1);
508    }
509
510    #[test]
511    fn from_u32_accepts_nonzero() {
512        let sv = SchemaVersion::from_u32(42).expect("nonzero");
513        assert_eq!(sv.get(), 42);
514    }
515
516    #[test]
517    fn from_u32_rejects_zero() {
518        let err = SchemaVersion::from_u32(0).expect_err("zero rejected");
519        assert!(matches!(err, ValueError::SchemaVersionZero));
520    }
521
522    #[test]
523    fn into_version_widens_to_nonzero_u64() {
524        let sv = SchemaVersion::from_u32(7).expect("nonzero");
525        let v: mnesis::Version = sv.into();
526        assert_eq!(v.as_u64(), 7);
527    }
528}
529
530#[cfg(test)]
531mod property_tests {
532    use super::*;
533    use bytes::Bytes;
534    use proptest::prelude::*;
535    use std::num::NonZeroU32;
536
537    proptest! {
538        #[test]
539        fn event_type_from_bytes_roundtrips_valid_utf8(s in "[a-zA-Z][a-zA-Z0-9_]{0,63}") {
540            let bytes = Bytes::from(s.clone().into_bytes());
541            let et = EventType::from_bytes(bytes).expect("valid");
542            prop_assert_eq!(et.as_str(), &s);
543        }
544
545        #[test]
546        fn payload_arbitrary_bytes_within_cap_accepted(
547            buf in proptest::collection::vec(any::<u8>(), 0..4096),
548        ) {
549            let p = Payload::from_bytes(Bytes::from(buf.clone())).expect("under cap");
550            prop_assert_eq!(p.as_slice(), buf.as_slice());
551        }
552
553        #[test]
554        fn metadata_nonempty_within_cap_accepted(
555            buf in proptest::collection::vec(any::<u8>(), 1..4096),
556        ) {
557            let m = Metadata::from_bytes(Bytes::from(buf.clone())).expect("nonempty, under cap");
558            prop_assert_eq!(m.as_slice(), buf.as_slice());
559        }
560
561        #[test]
562        fn event_type_invalid_utf8_always_rejected(
563            bytes in proptest::collection::vec(any::<u8>(), 1..256),
564        ) {
565            // Filter to bytes containing 0xFE or 0xFF, which are never valid
566            // UTF-8 start bytes — guarantees the input is invalid UTF-8.
567            prop_assume!(bytes.iter().any(|&b| b == 0xFEu8 || b == 0xFFu8));
568            let result = EventType::from_bytes(Bytes::from(bytes));
569            prop_assert!(result.is_err());
570        }
571
572        #[test]
573        fn schema_version_from_u32_roundtrips_nonzero(value in 1u32..) {
574            let sv = SchemaVersion::from_u32(value).expect("nonzero");
575            prop_assert_eq!(sv.get(), value);
576        }
577
578        #[test]
579        fn schema_version_widens_to_version(value in 1u32..) {
580            let sv = SchemaVersion::from_u32(value).expect("nonzero");
581            let v: mnesis::Version = sv.into();
582            prop_assert_eq!(v.as_u64(), u64::from(value));
583        }
584
585        #[test]
586        fn schema_version_ord_agrees_with_inner_u32_ord(
587            a in 1u32..,
588            b in 1u32..,
589        ) {
590            let sa = SchemaVersion::from_u32(a).expect("nonzero");
591            let sb = SchemaVersion::from_u32(b).expect("nonzero");
592            prop_assert_eq!(sa.cmp(&sb), a.cmp(&b));
593        }
594    }
595
596    // Pure unit tests for the structural boundaries that proptest can't
597    // exercise cheaply (4 GiB+ allocations).
598    #[test]
599    fn metadata_empty_always_rejected() {
600        let err = Metadata::from_bytes(Bytes::new()).expect_err("empty metadata always rejected");
601        assert!(matches!(err, ValueError::MetadataEmpty));
602    }
603
604    #[test]
605    fn schema_version_zero_rejected() {
606        let err = SchemaVersion::from_u32(0).expect_err("zero rejected");
607        assert!(matches!(err, ValueError::SchemaVersionZero));
608    }
609
610    #[test]
611    fn schema_version_initial_equals_one() {
612        assert_eq!(SchemaVersion::INITIAL.get(), 1);
613    }
614
615    #[test]
616    fn schema_version_initial_widens_to_version_one() {
617        let v: mnesis::Version = SchemaVersion::INITIAL.into();
618        assert_eq!(v.as_u64(), 1);
619    }
620
621    #[test]
622    fn schema_version_new_from_nonzero_construction() {
623        let nz = NonZeroU32::new(99).expect("nonzero");
624        let sv = SchemaVersion::new(nz);
625        assert_eq!(sv.get(), 99);
626    }
627}