Skip to main content

icydb_core/db/data/persisted_row/
mod.rs

1//! Module: data::persisted_row
2//! Responsibility: slot-oriented persisted-row seams over runtime row bytes.
3//! Does not own: row envelope versions, typed entity materialization, or query semantics.
4//! Boundary: commit/index planning, row writes, and typed materialization all
5//! consume the canonical slot-oriented persisted-row boundary here.
6
7mod codec;
8
9use crate::{
10    db::{
11        codec::serialize_row_payload,
12        data::{
13            CanonicalRow, DataKey, RawRow, StructuralRowDecodeError, StructuralRowFieldBytes,
14            decode_storage_key_field_bytes, decode_structural_field_by_kind_bytes,
15            decode_structural_value_storage_bytes, validate_structural_field_by_kind_bytes,
16            validate_structural_value_storage_bytes,
17        },
18        scalar_expr::compile_scalar_literal_expr_value,
19        schema::{field_type_from_model_kind, literal_matches_type},
20    },
21    error::InternalError,
22    model::{
23        entity::{EntityModel, resolve_field_slot, resolve_primary_key_slot},
24        field::{FieldKind, FieldModel, FieldStorageDecode, LeafCodec},
25    },
26    serialize::serialize,
27    traits::EntityKind,
28    value::{StorageKey, Value, ValueEnum},
29};
30use serde_cbor::{Value as CborValue, value::to_value as to_cbor_value};
31#[cfg(any(test, feature = "structural-read-metrics"))]
32use std::cell::{Cell, RefCell};
33use std::{borrow::Cow, cell::OnceCell, cmp::Ordering, collections::BTreeMap};
34
35use self::codec::{decode_scalar_slot_value, encode_scalar_slot_value};
36
37pub use self::codec::{
38    PersistedScalar, ScalarSlotValueRef, ScalarValueRef, decode_persisted_custom_many_slot_payload,
39    decode_persisted_custom_slot_payload, decode_persisted_non_null_slot_payload,
40    decode_persisted_option_scalar_slot_payload, decode_persisted_option_slot_payload,
41    decode_persisted_scalar_slot_payload, decode_persisted_slot_payload,
42    encode_persisted_custom_many_slot_payload, encode_persisted_custom_slot_payload,
43    encode_persisted_option_scalar_slot_payload, encode_persisted_scalar_slot_payload,
44    encode_persisted_slot_payload,
45};
46
47///
48/// FieldSlot
49///
50/// FieldSlot
51///
52/// FieldSlot is the structural stable slot reference used by the `0.64`
53/// patching path.
54/// It intentionally carries only the model-local slot index so field-level
55/// mutation stays structural instead of reintroducing typed entity helpers.
56///
57
58#[allow(dead_code)]
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub(in crate::db) struct FieldSlot {
61    index: usize,
62}
63
64#[allow(dead_code)]
65impl FieldSlot {
66    /// Resolve one stable field slot by runtime field name.
67    #[must_use]
68    pub(in crate::db) fn resolve(model: &'static EntityModel, field_name: &str) -> Option<Self> {
69        resolve_field_slot(model, field_name).map(|index| Self { index })
70    }
71
72    /// Build one stable field slot from an already validated index.
73    pub(in crate::db) fn from_index(
74        model: &'static EntityModel,
75        index: usize,
76    ) -> Result<Self, InternalError> {
77        field_model_for_slot(model, index)?;
78
79        Ok(Self { index })
80    }
81
82    /// Return the stable slot index inside `EntityModel::fields`.
83    #[must_use]
84    pub(in crate::db) const fn index(self) -> usize {
85        self.index
86    }
87}
88
89///
90/// FieldUpdate
91///
92/// FieldUpdate
93///
94/// FieldUpdate carries one ordered field-level mutation over the structural
95/// persisted-row boundary.
96/// `UpdatePatch` applies these entries in order and last write wins for the
97/// same slot.
98///
99
100#[allow(dead_code)]
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub(in crate::db) struct FieldUpdate {
103    slot: FieldSlot,
104    value: Value,
105}
106
107#[allow(dead_code)]
108impl FieldUpdate {
109    /// Build one field-level structural update.
110    #[must_use]
111    pub(in crate::db) const fn new(slot: FieldSlot, value: Value) -> Self {
112        Self { slot, value }
113    }
114
115    /// Return the stable target slot.
116    #[must_use]
117    pub(in crate::db) const fn slot(&self) -> FieldSlot {
118        self.slot
119    }
120
121    /// Return the runtime value payload for this update.
122    #[must_use]
123    pub(in crate::db) const fn value(&self) -> &Value {
124        &self.value
125    }
126}
127
128///
129/// UpdatePatch
130///
131/// UpdatePatch
132///
133/// UpdatePatch is the ordered structural mutation program applied to one
134/// persisted row.
135/// This is the phase-1 `0.64` patch container: it updates slot values
136/// structurally and then re-encodes the full row.
137///
138
139#[derive(Clone, Debug, Default, Eq, PartialEq)]
140pub struct UpdatePatch {
141    entries: Vec<FieldUpdate>,
142}
143
144impl UpdatePatch {
145    /// Build one empty patch.
146    #[must_use]
147    pub const fn new() -> Self {
148        Self {
149            entries: Vec::new(),
150        }
151    }
152
153    /// Append one structural field update in declaration order.
154    #[must_use]
155    pub(in crate::db) fn set(mut self, slot: FieldSlot, value: Value) -> Self {
156        self.entries.push(FieldUpdate::new(slot, value));
157        self
158    }
159
160    /// Resolve one field name and append its structural update.
161    pub fn set_field(
162        self,
163        model: &'static EntityModel,
164        field_name: &str,
165        value: Value,
166    ) -> Result<Self, InternalError> {
167        let Some(slot) = FieldSlot::resolve(model, field_name) else {
168            return Err(InternalError::mutation_structural_field_unknown(
169                model.path(),
170                field_name,
171            ));
172        };
173
174        Ok(self.set(slot, value))
175    }
176
177    /// Borrow the ordered field updates carried by this patch.
178    #[must_use]
179    pub(in crate::db) const fn entries(&self) -> &[FieldUpdate] {
180        self.entries.as_slice()
181    }
182
183    /// Return whether this patch carries no field updates.
184    #[must_use]
185    pub(in crate::db) const fn is_empty(&self) -> bool {
186        self.entries.is_empty()
187    }
188}
189
190///
191/// SerializedFieldUpdate
192///
193/// SerializedFieldUpdate
194///
195/// SerializedFieldUpdate carries one ordered field-level mutation after the
196/// owning persisted-row field codec has already lowered the runtime `Value`
197/// into canonical slot payload bytes.
198/// This lets later patch-application stages consume one mechanical slot-patch
199/// artifact instead of rebuilding per-field encode dispatch.
200///
201
202#[allow(dead_code)]
203#[derive(Clone, Debug, Eq, PartialEq)]
204pub(in crate::db) struct SerializedFieldUpdate {
205    slot: FieldSlot,
206    payload: Vec<u8>,
207}
208
209#[allow(dead_code)]
210impl SerializedFieldUpdate {
211    /// Build one serialized structural field update.
212    #[must_use]
213    pub(in crate::db) const fn new(slot: FieldSlot, payload: Vec<u8>) -> Self {
214        Self { slot, payload }
215    }
216
217    /// Return the stable target slot.
218    #[must_use]
219    pub(in crate::db) const fn slot(&self) -> FieldSlot {
220        self.slot
221    }
222
223    /// Borrow the canonical slot payload bytes for this update when present.
224    #[must_use]
225    pub(in crate::db) const fn payload(&self) -> &[u8] {
226        self.payload.as_slice()
227    }
228}
229
230///
231/// SerializedUpdatePatch
232///
233/// SerializedUpdatePatch
234///
235/// SerializedUpdatePatch is the canonical serialized form of `UpdatePatch`
236/// over persisted-row slot payload bytes.
237/// This is the structural patch artifact later write-path stages can stage or
238/// replay without re-entering field-contract encode logic.
239///
240
241#[allow(dead_code)]
242#[derive(Clone, Debug, Default, Eq, PartialEq)]
243pub(in crate::db) struct SerializedUpdatePatch {
244    entries: Vec<SerializedFieldUpdate>,
245}
246
247#[allow(dead_code)]
248impl SerializedUpdatePatch {
249    /// Build one serialized patch from already encoded slot payloads.
250    #[must_use]
251    pub(in crate::db) const fn new(entries: Vec<SerializedFieldUpdate>) -> Self {
252        Self { entries }
253    }
254
255    /// Borrow the ordered serialized field updates carried by this patch.
256    #[must_use]
257    pub(in crate::db) const fn entries(&self) -> &[SerializedFieldUpdate] {
258        self.entries.as_slice()
259    }
260
261    /// Return whether this serialized patch carries no field updates.
262    #[must_use]
263    pub(in crate::db) const fn is_empty(&self) -> bool {
264        self.entries.is_empty()
265    }
266}
267
268///
269/// SlotReader
270///
271/// SlotReader exposes one persisted row as stable slot-addressable fields.
272/// Callers may inspect field presence, borrow raw field bytes, or decode one
273/// field value on demand.
274///
275
276pub trait SlotReader {
277    /// Return the structural model that owns this slot mapping.
278    fn model(&self) -> &'static EntityModel;
279
280    /// Return whether the given slot is present in the persisted row.
281    fn has(&self, slot: usize) -> bool;
282
283    /// Borrow the raw persisted payload for one slot when present.
284    fn get_bytes(&self, slot: usize) -> Option<&[u8]>;
285
286    /// Decode one slot as a scalar leaf when the field model declares a scalar codec.
287    fn get_scalar(&self, slot: usize) -> Result<Option<ScalarSlotValueRef<'_>>, InternalError>;
288
289    /// Decode one slot value on demand using the field contract declared by the model.
290    fn get_value(&mut self, slot: usize) -> Result<Option<Value>, InternalError>;
291}
292
293///
294/// CanonicalSlotReader
295///
296/// CanonicalSlotReader
297///
298/// CanonicalSlotReader is the stricter structural row-reader contract used
299/// once `0.65` canonical-row invariants are in force.
300/// Declared slots must already exist, so callers can fail closed on missing
301/// payloads instead of carrying absent-slot fallback branches.
302///
303
304pub(in crate::db) trait CanonicalSlotReader: SlotReader {
305    /// Borrow one declared slot payload, erroring when the persisted row is not canonical.
306    fn required_bytes(&self, slot: usize) -> Result<&[u8], InternalError> {
307        let field = field_model_for_slot(self.model(), slot)?;
308
309        self.get_bytes(slot)
310            .ok_or_else(|| InternalError::persisted_row_declared_field_missing(field.name()))
311    }
312
313    /// Read one scalar slot through the structural fast path without allowing
314    /// declared-slot absence.
315    fn required_scalar(&self, slot: usize) -> Result<ScalarSlotValueRef<'_>, InternalError> {
316        let field = field_model_for_slot(self.model(), slot)?;
317        debug_assert!(matches!(field.leaf_codec(), LeafCodec::Scalar(_)));
318
319        self.get_scalar(slot)?
320            .ok_or_else(|| InternalError::persisted_row_declared_field_missing(field.name()))
321    }
322
323    /// Decode one declared slot through the owning field contract without
324    /// allowing absent payloads.
325    fn required_value_by_contract(&self, slot: usize) -> Result<Value, InternalError> {
326        decode_slot_value_from_bytes(self.model(), slot, self.required_bytes(slot)?)
327    }
328
329    /// Borrow one declared slot value when the concrete reader already owns a
330    /// validated decoded cache, while preserving the existing owned fallback
331    /// for reader implementations that still decode on demand.
332    fn required_value_by_contract_cow(&self, slot: usize) -> Result<Cow<'_, Value>, InternalError> {
333        Ok(Cow::Owned(self.required_value_by_contract(slot)?))
334    }
335}
336
337///
338/// SlotWriter
339///
340/// SlotWriter is the canonical row-container output seam used by persisted-row
341/// writers.
342///
343
344pub trait SlotWriter {
345    /// Record one slot payload for the current row.
346    fn write_slot(&mut self, slot: usize, payload: Option<&[u8]>) -> Result<(), InternalError>;
347
348    /// Record one scalar slot payload using the canonical scalar leaf envelope.
349    fn write_scalar(
350        &mut self,
351        slot: usize,
352        value: ScalarSlotValueRef<'_>,
353    ) -> Result<(), InternalError> {
354        let payload = encode_scalar_slot_value(value);
355
356        self.write_slot(slot, Some(payload.as_slice()))
357    }
358}
359
360// Resolve one staged slot cell by layout index before writer-specific payload handling.
361fn slot_cell_mut<T>(slots: &mut [T], slot: usize) -> Result<&mut T, InternalError> {
362    slots.get_mut(slot).ok_or_else(|| {
363        InternalError::persisted_row_encode_failed(
364            format!("slot {slot} is outside the row layout",),
365        )
366    })
367}
368
369// Reject slot clears at the canonical slot-image staging boundary while keeping
370// writer-specific error wording at the call site.
371fn required_slot_payload_bytes<'a>(
372    model: &'static EntityModel,
373    writer_label: &str,
374    slot: usize,
375    payload: Option<&'a [u8]>,
376) -> Result<&'a [u8], InternalError> {
377    payload.ok_or_else(|| {
378        InternalError::persisted_row_encode_failed(format!(
379            "{writer_label} cannot clear slot {slot} for entity '{}'",
380            model.path()
381        ))
382    })
383}
384
385// Encode one fixed-width slot table plus concatenated slot payload bytes into
386// the canonical row payload container.
387fn encode_slot_payload_from_parts(
388    slot_count: usize,
389    slot_table: &[(u32, u32)],
390    payload_bytes: &[u8],
391) -> Result<Vec<u8>, InternalError> {
392    let field_count = u16::try_from(slot_count).map_err(|_| {
393        InternalError::persisted_row_encode_failed(format!(
394            "field count {slot_count} exceeds u16 slot table capacity",
395        ))
396    })?;
397    let mut encoded = Vec::with_capacity(
398        usize::from(field_count) * (u32::BITS as usize / 4) + 2 + payload_bytes.len(),
399    );
400    encoded.extend_from_slice(&field_count.to_be_bytes());
401    for (start, len) in slot_table {
402        encoded.extend_from_slice(&start.to_be_bytes());
403        encoded.extend_from_slice(&len.to_be_bytes());
404    }
405    encoded.extend_from_slice(payload_bytes);
406
407    Ok(encoded)
408}
409
410///
411/// PersistedRow
412///
413/// PersistedRow is the derive-owned bridge between typed entities and
414/// slot-addressable persisted rows.
415/// It owns entity-specific materialization/default semantics while runtime
416/// paths stay structural at the row boundary.
417///
418
419pub trait PersistedRow: EntityKind + Sized {
420    /// Materialize one typed entity from one slot reader.
421    fn materialize_from_slots(slots: &mut dyn SlotReader) -> Result<Self, InternalError>;
422
423    /// Write one typed entity into one slot writer.
424    fn write_slots(&self, out: &mut dyn SlotWriter) -> Result<(), InternalError>;
425
426    /// Decode one slot value needed by structural planner/projection consumers.
427    fn project_slot(slots: &mut dyn SlotReader, slot: usize) -> Result<Option<Value>, InternalError>
428    where
429        Self: crate::traits::FieldProjection,
430    {
431        let entity = Self::materialize_from_slots(slots)?;
432
433        Ok(<Self as crate::traits::FieldProjection>::get_value_by_index(&entity, slot))
434    }
435}
436
437/// Decode one slot value through the declared field contract without routing
438/// through `SlotReader::get_value`.
439#[cfg(test)]
440pub(in crate::db) fn decode_slot_value_by_contract(
441    slots: &dyn SlotReader,
442    slot: usize,
443) -> Result<Option<Value>, InternalError> {
444    let Some(raw_value) = slots.get_bytes(slot) else {
445        return Ok(None);
446    };
447
448    decode_slot_value_from_bytes(slots.model(), slot, raw_value).map(Some)
449}
450
451/// Decode one structural slot payload using the owning model field contract.
452///
453/// This is the canonical field-level decode boundary for persisted-row bytes.
454/// Higher-level row readers may still cache decoded values, but they should not
455/// rebuild scalar-vs-CBOR field dispatch themselves.
456pub(in crate::db) fn decode_slot_value_from_bytes(
457    model: &'static EntityModel,
458    slot: usize,
459    raw_value: &[u8],
460) -> Result<Value, InternalError> {
461    let field = field_model_for_slot(model, slot)?;
462
463    decode_slot_value_for_field(field, raw_value)
464}
465
466// Decode one structural slot payload once the owning field contract has
467// already been resolved.
468fn decode_slot_value_for_field(
469    field: &FieldModel,
470    raw_value: &[u8],
471) -> Result<Value, InternalError> {
472    match field.leaf_codec() {
473        LeafCodec::Scalar(codec) => match decode_scalar_slot_value(raw_value, codec, field.name())?
474        {
475            ScalarSlotValueRef::Null => Ok(Value::Null),
476            ScalarSlotValueRef::Value(value) => Ok(value.into_value()),
477        },
478        LeafCodec::CborFallback => {
479            if field.nullable() && is_canonical_nullable_cbor_null_payload(raw_value) {
480                return Ok(Value::Null);
481            }
482
483            decode_non_scalar_slot_value(raw_value, field)
484        }
485    }
486}
487
488/// Encode one structural slot value using the owning model field contract.
489///
490/// This is the initial `0.64` write-side field-codec boundary. It currently
491/// covers:
492/// - scalar leaf slots
493/// - `FieldStorageDecode::Value` slots
494///
495/// Composite `ByKind` field encoding remains a follow-up slice so the runtime
496/// can add one structural encoder owner instead of quietly rebuilding typed
497/// per-field branches.
498#[allow(dead_code)]
499pub(in crate::db) fn encode_slot_value_from_value(
500    model: &'static EntityModel,
501    slot: usize,
502    value: &Value,
503) -> Result<Vec<u8>, InternalError> {
504    let field = field_model_for_slot(model, slot)?;
505    ensure_slot_value_matches_field_contract(field, value)?;
506
507    match field.storage_decode() {
508        FieldStorageDecode::Value => serialize(value)
509            .map_err(|err| InternalError::persisted_row_field_encode_failed(field.name(), err)),
510        FieldStorageDecode::ByKind => match field.leaf_codec() {
511            LeafCodec::Scalar(_) => {
512                let scalar = compile_scalar_literal_expr_value(value).ok_or_else(|| {
513                    InternalError::persisted_row_field_encode_failed(
514                        field.name(),
515                        format!(
516                            "field kind {:?} requires a scalar runtime value, found {value:?}",
517                            field.kind()
518                        ),
519                    )
520                })?;
521
522                Ok(encode_scalar_slot_value(scalar.as_slot_value_ref()))
523            }
524            LeafCodec::CborFallback => {
525                encode_structural_field_bytes_by_kind(field.kind(), value, field.name())
526            }
527        },
528    }
529}
530
531// Decode one slot payload and immediately re-encode it through the current
532// field contract so every row-emission path normalizes bytes at the boundary.
533fn canonicalize_slot_payload(
534    model: &'static EntityModel,
535    slot: usize,
536    raw_value: &[u8],
537) -> Result<Vec<u8>, InternalError> {
538    let value = decode_slot_value_from_bytes(model, slot, raw_value)?;
539
540    encode_slot_value_from_value(model, slot, &value)
541}
542
543// Build one dense canonical slot image from any slot-addressable payload source.
544// Callers keep ownership of missing-slot policy while this helper centralizes
545// the slot-by-slot canonicalization loop.
546fn dense_canonical_slot_image_from_payload_source<'a, F>(
547    model: &'static EntityModel,
548    mut payload_for_slot: F,
549) -> Result<Vec<Vec<u8>>, InternalError>
550where
551    F: FnMut(usize) -> Result<&'a [u8], InternalError>,
552{
553    let mut slot_payloads = Vec::with_capacity(model.fields().len());
554
555    for slot in 0..model.fields().len() {
556        let payload = payload_for_slot(slot)?;
557        slot_payloads.push(canonicalize_slot_payload(model, slot, payload)?);
558    }
559
560    Ok(slot_payloads)
561}
562
563// Build one dense canonical slot image from already-decoded runtime values.
564// This keeps row-emission paths from re-decoding raw slot bytes when a caller
565// already owns the validated structural value cache.
566fn dense_canonical_slot_image_from_value_source<'a, F>(
567    model: &'static EntityModel,
568    mut value_for_slot: F,
569) -> Result<Vec<Vec<u8>>, InternalError>
570where
571    F: FnMut(usize) -> Result<Cow<'a, Value>, InternalError>,
572{
573    let mut slot_payloads = Vec::with_capacity(model.fields().len());
574
575    for slot in 0..model.fields().len() {
576        let value = value_for_slot(slot)?;
577        slot_payloads.push(encode_slot_value_from_value(model, slot, value.as_ref())?);
578    }
579
580    Ok(slot_payloads)
581}
582
583// Emit one raw row from a dense canonical slot image.
584fn emit_raw_row_from_slot_payloads(
585    model: &'static EntityModel,
586    slot_payloads: &[Vec<u8>],
587) -> Result<CanonicalRow, InternalError> {
588    if slot_payloads.len() != model.fields().len() {
589        return Err(InternalError::persisted_row_encode_failed(format!(
590            "canonical slot image expected {} slots for entity '{}', found {}",
591            model.fields().len(),
592            model.path(),
593            slot_payloads.len()
594        )));
595    }
596
597    let payload_capacity = slot_payloads
598        .iter()
599        .try_fold(0usize, |len, payload| len.checked_add(payload.len()))
600        .ok_or_else(|| {
601            InternalError::persisted_row_encode_failed(
602                "canonical slot image payload length overflow",
603            )
604        })?;
605    let mut payload_bytes = Vec::with_capacity(payload_capacity);
606    let mut slot_table = Vec::with_capacity(slot_payloads.len());
607
608    // Phase 1: flatten the already canonicalized dense slot image directly so
609    // row re-emission does not clone each slot payload back through the
610    // mutable slot-writer staging buffer first.
611    for (slot, payload) in slot_payloads.iter().enumerate() {
612        let start = u32::try_from(payload_bytes.len()).map_err(|_| {
613            InternalError::persisted_row_encode_failed(format!(
614                "canonical slot payload start exceeds u32 range: slot={slot}",
615            ))
616        })?;
617        let len = u32::try_from(payload.len()).map_err(|_| {
618            InternalError::persisted_row_encode_failed(format!(
619                "canonical slot payload length exceeds u32 range: slot={slot}",
620            ))
621        })?;
622        payload_bytes.extend_from_slice(payload.as_slice());
623        slot_table.push((start, len));
624    }
625
626    // Phase 2: wrap the canonical slot container in the shared row envelope.
627    let row_payload =
628        encode_slot_payload_from_parts(slot_payloads.len(), slot_table.as_slice(), &payload_bytes)?;
629    let encoded = serialize_row_payload(row_payload)?;
630    let raw_row = RawRow::from_untrusted_bytes(encoded).map_err(InternalError::from)?;
631
632    Ok(CanonicalRow::from_canonical_raw_row(raw_row))
633}
634
635// Build one dense canonical slot image from a serialized patch, failing closed
636// when any declared slot is missing or any payload is non-canonical.
637fn dense_canonical_slot_image_from_serialized_patch(
638    model: &'static EntityModel,
639    patch: &SerializedUpdatePatch,
640) -> Result<Vec<Vec<u8>>, InternalError> {
641    let patch_payloads = serialized_patch_payload_by_slot(model, patch)?;
642
643    dense_canonical_slot_image_from_payload_source(model, |slot| {
644        patch_payloads[slot].ok_or_else(|| {
645            InternalError::persisted_row_encode_failed(format!(
646                "serialized patch did not emit slot {slot} for entity '{}'",
647                model.path()
648            ))
649        })
650    })
651}
652
653/// Build one canonical row from one serialized structural patch that already
654/// describes a full logical row image.
655pub(in crate::db) fn canonical_row_from_serialized_update_patch(
656    model: &'static EntityModel,
657    patch: &SerializedUpdatePatch,
658) -> Result<CanonicalRow, InternalError> {
659    let slot_payloads = dense_canonical_slot_image_from_serialized_patch(model, patch)?;
660
661    emit_raw_row_from_slot_payloads(model, slot_payloads.as_slice())
662}
663
664/// Build one canonical row directly from one typed entity slot writer.
665pub(in crate::db) fn canonical_row_from_entity<E>(entity: &E) -> Result<CanonicalRow, InternalError>
666where
667    E: PersistedRow,
668{
669    let mut writer = SlotBufferWriter::for_model(E::MODEL);
670
671    // Phase 1: let the derive-owned slot writer emit the complete typed row image.
672    entity.write_slots(&mut writer)?;
673
674    // Phase 2: wrap the canonical slot container in the shared row envelope.
675    let encoded = serialize_row_payload(writer.finish()?)?;
676    let raw_row = RawRow::from_untrusted_bytes(encoded).map_err(InternalError::from)?;
677
678    Ok(CanonicalRow::from_canonical_raw_row(raw_row))
679}
680
681/// Build one canonical row from one already-decoded structural slot reader.
682pub(in crate::db) fn canonical_row_from_structural_slot_reader(
683    row_fields: &StructuralSlotReader<'_>,
684) -> Result<CanonicalRow, InternalError> {
685    // Phase 1: re-encode every declared slot from the already-decoded cache so
686    // commit preparation does not re-enter raw field-byte decode after the
687    // structural reader has already validated the row.
688    let slot_payloads = dense_canonical_slot_image_from_value_source(row_fields.model, |slot| {
689        row_fields
690            .required_cached_value(slot)
691            .map(Cow::Borrowed)
692            .map_err(|_| {
693                InternalError::persisted_row_encode_failed(format!(
694                    "slot {slot} is missing from the structural value cache for entity '{}'",
695                    row_fields.model.path()
696                ))
697            })
698    })?;
699
700    // Phase 2: re-emit the full image through the single row-emission owner.
701    emit_raw_row_from_slot_payloads(row_fields.model, slot_payloads.as_slice())
702}
703
704// Rebuild one full canonical row image from an existing raw row before it
705// crosses a storage write boundary.
706pub(in crate::db) fn canonical_row_from_raw_row(
707    model: &'static EntityModel,
708    raw_row: &RawRow,
709) -> Result<CanonicalRow, InternalError> {
710    let field_bytes = StructuralRowFieldBytes::from_raw_row(raw_row, model)
711        .map_err(StructuralRowDecodeError::into_internal_error)?;
712
713    // Phase 1: canonicalize every declared slot from the existing row image.
714    let slot_payloads = dense_canonical_slot_image_from_payload_source(model, |slot| {
715        field_bytes.field(slot).ok_or_else(|| {
716            InternalError::persisted_row_encode_failed(format!(
717                "slot {slot} is missing from the baseline row for entity '{}'",
718                model.path()
719            ))
720        })
721    })?;
722
723    // Phase 2: re-emit the full image through the single row-emission owner.
724    emit_raw_row_from_slot_payloads(model, slot_payloads.as_slice())
725}
726
727// Rewrap one row already loaded from storage as a canonical write token.
728pub(in crate::db) const fn canonical_row_from_stored_raw_row(raw_row: RawRow) -> CanonicalRow {
729    CanonicalRow::from_canonical_raw_row(raw_row)
730}
731
732/// Apply one ordered structural patch to one raw row using the current
733/// persisted-row field codec authority.
734#[allow(dead_code)]
735pub(in crate::db) fn apply_update_patch_to_raw_row(
736    model: &'static EntityModel,
737    raw_row: &RawRow,
738    patch: &UpdatePatch,
739) -> Result<CanonicalRow, InternalError> {
740    let serialized_patch = serialize_update_patch_fields(model, patch)?;
741
742    apply_serialized_update_patch_to_raw_row(model, raw_row, &serialized_patch)
743}
744
745/// Serialize one ordered structural patch into canonical slot payload bytes.
746///
747/// This is the phase-1 partial-serialization seam for `0.64`: later mutation
748/// stages can stage or replay one field patch without rebuilding the runtime
749/// value-to-bytes contract per consumer.
750#[allow(dead_code)]
751pub(in crate::db) fn serialize_update_patch_fields(
752    model: &'static EntityModel,
753    patch: &UpdatePatch,
754) -> Result<SerializedUpdatePatch, InternalError> {
755    if patch.is_empty() {
756        return Ok(SerializedUpdatePatch::default());
757    }
758
759    let mut entries = Vec::with_capacity(patch.entries().len());
760
761    // Phase 1: validate and encode each ordered field update through the
762    // canonical slot codec owner.
763    for entry in patch.entries() {
764        let slot = entry.slot();
765        let payload = encode_slot_value_from_value(model, slot.index(), entry.value())?;
766        entries.push(SerializedFieldUpdate::new(slot, payload));
767    }
768
769    Ok(SerializedUpdatePatch::new(entries))
770}
771
772/// Serialize one full typed entity image into the canonical serialized patch
773/// artifact used by row-boundary patch replay.
774///
775/// This keeps typed save/update APIs on the existing surface while moving the
776/// actual after-image staging onto the structural slot-patch boundary.
777#[allow(dead_code)]
778pub(in crate::db) fn serialize_entity_slots_as_update_patch<E>(
779    entity: &E,
780) -> Result<SerializedUpdatePatch, InternalError>
781where
782    E: PersistedRow,
783{
784    let mut writer = SerializedPatchWriter::for_model(E::MODEL);
785
786    // Phase 1: let the derive-owned persisted-row writer emit the complete
787    // structural slot image for this entity.
788    entity.write_slots(&mut writer)?;
789
790    // Phase 2: require a dense slot image so save/update replay remains
791    // equivalent to the existing full-row write semantics.
792    writer.finish_complete()
793}
794
795/// Apply one serialized structural patch to one raw row.
796///
797/// This mechanical replay step no longer owns any `Value -> bytes` dispatch.
798/// It only replays already encoded slot payloads over the current row layout.
799#[allow(dead_code)]
800pub(in crate::db) fn apply_serialized_update_patch_to_raw_row(
801    model: &'static EntityModel,
802    raw_row: &RawRow,
803    patch: &SerializedUpdatePatch,
804) -> Result<CanonicalRow, InternalError> {
805    if patch.is_empty() {
806        return canonical_row_from_raw_row(model, raw_row);
807    }
808
809    let field_bytes = StructuralRowFieldBytes::from_raw_row(raw_row, model)
810        .map_err(StructuralRowDecodeError::into_internal_error)?;
811    let patch_payloads = serialized_patch_payload_by_slot(model, patch)?;
812
813    // Phase 1: replay the current row layout slot-by-slot.
814    // Both patch and baseline bytes are normalized through the field contract
815    // so no opaque payload can cross into the emitted row image.
816    let slot_payloads = dense_canonical_slot_image_from_payload_source(model, |slot| {
817        if let Some(payload) = patch_payloads[slot] {
818            Ok(payload)
819        } else {
820            field_bytes.field(slot).ok_or_else(|| {
821                InternalError::persisted_row_encode_failed(format!(
822                    "slot {slot} is missing from the baseline row for entity '{}'",
823                    model.path()
824                ))
825            })
826        }
827    })?;
828
829    // Phase 2: emit the rebuilt row through the single row-construction owner.
830    emit_raw_row_from_slot_payloads(model, slot_payloads.as_slice())
831}
832
833// Decode one non-scalar slot through the exact persisted contract declared by
834// the field model.
835fn decode_non_scalar_slot_value(
836    raw_value: &[u8],
837    field: &FieldModel,
838) -> Result<Value, InternalError> {
839    let decoded = match field.storage_decode() {
840        crate::model::field::FieldStorageDecode::ByKind => {
841            decode_structural_field_by_kind_bytes(raw_value, field.kind())
842        }
843        crate::model::field::FieldStorageDecode::Value => {
844            decode_structural_value_storage_bytes(raw_value)
845        }
846    };
847
848    decoded.map_err(|err| {
849        InternalError::persisted_row_field_kind_decode_failed(field.name(), field.kind(), err)
850    })
851}
852
853// Validate one non-scalar slot through the exact persisted contract declared
854// by the field model without eagerly building the final runtime `Value`.
855fn validate_non_scalar_slot_value(
856    raw_value: &[u8],
857    field: &FieldModel,
858) -> Result<(), InternalError> {
859    if field.nullable() && is_canonical_nullable_cbor_null_payload(raw_value) {
860        return Ok(());
861    }
862
863    let validated = match field.storage_decode() {
864        crate::model::field::FieldStorageDecode::ByKind => {
865            validate_structural_field_by_kind_bytes(raw_value, field.kind())
866        }
867        crate::model::field::FieldStorageDecode::Value => {
868            validate_structural_value_storage_bytes(raw_value)
869        }
870    };
871
872    validated.map_err(|err| {
873        InternalError::persisted_row_field_kind_decode_failed(field.name(), field.kind(), err)
874    })
875}
876
877// Detect the canonical explicit-null payload used for nullable CBOR-backed
878// fields before field-kind-specific structural decode requires a concrete shape.
879fn is_canonical_nullable_cbor_null_payload(raw_value: &[u8]) -> bool {
880    raw_value == [0xF6]
881}
882
883// Validate one runtime value against the persisted field contract before field-
884// level structural encoding writes bytes into a row slot.
885#[allow(dead_code)]
886fn ensure_slot_value_matches_field_contract(
887    field: &FieldModel,
888    value: &Value,
889) -> Result<(), InternalError> {
890    if matches!(value, Value::Null) {
891        if field.nullable() {
892            return Ok(());
893        }
894
895        return Err(InternalError::persisted_row_field_encode_failed(
896            field.name(),
897            "required field cannot store null",
898        ));
899    }
900
901    // `FieldStorageDecode::Value` fields persist the generic `Value` envelope
902    // directly, so storage-side validation must accept structured leaves nested
903    // under collection contracts instead of reusing the predicate literal gate.
904    if matches!(field.storage_decode(), FieldStorageDecode::Value) {
905        if !storage_value_matches_field_kind(field.kind(), value) {
906            return Err(InternalError::persisted_row_field_encode_failed(
907                field.name(),
908                format!(
909                    "field kind {:?} does not accept runtime value {value:?}",
910                    field.kind()
911                ),
912            ));
913        }
914
915        ensure_decimal_scale_matches(field.name(), field.kind(), value)?;
916
917        return ensure_value_is_deterministic_for_storage(field.name(), field.kind(), value);
918    }
919
920    let field_type = field_type_from_model_kind(&field.kind());
921    if !literal_matches_type(value, &field_type) {
922        return Err(InternalError::persisted_row_field_encode_failed(
923            field.name(),
924            format!(
925                "field kind {:?} does not accept runtime value {value:?}",
926                field.kind()
927            ),
928        ));
929    }
930
931    ensure_decimal_scale_matches(field.name(), field.kind(), value)?;
932    ensure_value_is_deterministic_for_storage(field.name(), field.kind(), value)
933}
934
935// Match one runtime value against the semantic field kind used by value-backed
936// storage. Unlike predicate literals, non-queryable structured leaves are valid
937// persisted payloads when they arrive as canonical `Value::List` / `Value::Map`
938// shapes.
939fn storage_value_matches_field_kind(kind: FieldKind, value: &Value) -> bool {
940    match (kind, value) {
941        (FieldKind::Account, Value::Account(_))
942        | (FieldKind::Blob, Value::Blob(_))
943        | (FieldKind::Bool, Value::Bool(_))
944        | (FieldKind::Date, Value::Date(_))
945        | (FieldKind::Decimal { .. }, Value::Decimal(_))
946        | (FieldKind::Duration, Value::Duration(_))
947        | (FieldKind::Enum { .. }, Value::Enum(_))
948        | (FieldKind::Float32, Value::Float32(_))
949        | (FieldKind::Float64, Value::Float64(_))
950        | (FieldKind::Int, Value::Int(_))
951        | (FieldKind::Int128, Value::Int128(_))
952        | (FieldKind::IntBig, Value::IntBig(_))
953        | (FieldKind::Principal, Value::Principal(_))
954        | (FieldKind::Subaccount, Value::Subaccount(_))
955        | (FieldKind::Text, Value::Text(_))
956        | (FieldKind::Timestamp, Value::Timestamp(_))
957        | (FieldKind::Uint, Value::Uint(_))
958        | (FieldKind::Uint128, Value::Uint128(_))
959        | (FieldKind::UintBig, Value::UintBig(_))
960        | (FieldKind::Ulid, Value::Ulid(_))
961        | (FieldKind::Unit, Value::Unit)
962        | (FieldKind::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
963        (FieldKind::Relation { key_kind, .. }, value) => {
964            storage_value_matches_field_kind(*key_kind, value)
965        }
966        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
967            .iter()
968            .all(|item| storage_value_matches_field_kind(*inner, item)),
969        (FieldKind::Map { key, value }, Value::Map(entries)) => {
970            if Value::validate_map_entries(entries.as_slice()).is_err() {
971                return false;
972            }
973
974            entries.iter().all(|(entry_key, entry_value)| {
975                storage_value_matches_field_kind(*key, entry_key)
976                    && storage_value_matches_field_kind(*value, entry_value)
977            })
978        }
979        _ => false,
980    }
981}
982
983// Enforce fixed decimal scales through nested collection/map shapes before a
984// field-level patch value is persisted.
985#[allow(dead_code)]
986fn ensure_decimal_scale_matches(
987    field_name: &str,
988    kind: FieldKind,
989    value: &Value,
990) -> Result<(), InternalError> {
991    if matches!(value, Value::Null) {
992        return Ok(());
993    }
994
995    match (kind, value) {
996        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
997            if decimal.scale() != scale {
998                return Err(InternalError::persisted_row_field_encode_failed(
999                    field_name,
1000                    format!(
1001                        "decimal scale mismatch: expected {scale}, found {}",
1002                        decimal.scale()
1003                    ),
1004                ));
1005            }
1006
1007            Ok(())
1008        }
1009        (FieldKind::Relation { key_kind, .. }, value) => {
1010            ensure_decimal_scale_matches(field_name, *key_kind, value)
1011        }
1012        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1013            for item in items {
1014                ensure_decimal_scale_matches(field_name, *inner, item)?;
1015            }
1016
1017            Ok(())
1018        }
1019        (
1020            FieldKind::Map {
1021                key,
1022                value: map_value,
1023            },
1024            Value::Map(entries),
1025        ) => {
1026            for (entry_key, entry_value) in entries {
1027                ensure_decimal_scale_matches(field_name, *key, entry_key)?;
1028                ensure_decimal_scale_matches(field_name, *map_value, entry_value)?;
1029            }
1030
1031            Ok(())
1032        }
1033        _ => Ok(()),
1034    }
1035}
1036
1037// Enforce the canonical persisted ordering rules for set/map shapes before one
1038// field-level patch value becomes row bytes.
1039#[allow(dead_code)]
1040fn ensure_value_is_deterministic_for_storage(
1041    field_name: &str,
1042    kind: FieldKind,
1043    value: &Value,
1044) -> Result<(), InternalError> {
1045    match (kind, value) {
1046        (FieldKind::Set(_), Value::List(items)) => {
1047            for pair in items.windows(2) {
1048                let [left, right] = pair else {
1049                    continue;
1050                };
1051                if Value::canonical_cmp(left, right) != Ordering::Less {
1052                    return Err(InternalError::persisted_row_field_encode_failed(
1053                        field_name,
1054                        "set payload must already be canonical and deduplicated",
1055                    ));
1056                }
1057            }
1058
1059            Ok(())
1060        }
1061        (FieldKind::Map { .. }, Value::Map(entries)) => {
1062            Value::validate_map_entries(entries.as_slice())
1063                .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err))?;
1064
1065            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1066                return Err(InternalError::persisted_row_field_encode_failed(
1067                    field_name,
1068                    "map payload must already be canonical and deduplicated",
1069                ));
1070            }
1071
1072            Ok(())
1073        }
1074        _ => Ok(()),
1075    }
1076}
1077
1078// Materialize the last-write-wins serialized patch view indexed by stable slot.
1079fn serialized_patch_payload_by_slot<'a>(
1080    model: &'static EntityModel,
1081    patch: &'a SerializedUpdatePatch,
1082) -> Result<Vec<Option<&'a [u8]>>, InternalError> {
1083    let mut payloads = vec![None; model.fields().len()];
1084
1085    for entry in patch.entries() {
1086        let slot = entry.slot().index();
1087        field_model_for_slot(model, slot)?;
1088        payloads[slot] = Some(entry.payload());
1089    }
1090
1091    Ok(payloads)
1092}
1093
1094// Encode one `ByKind` field payload into the raw CBOR shape expected by the
1095// structural field decoder.
1096fn encode_structural_field_bytes_by_kind(
1097    kind: FieldKind,
1098    value: &Value,
1099    field_name: &str,
1100) -> Result<Vec<u8>, InternalError> {
1101    let cbor_value = encode_structural_field_cbor_by_kind(kind, value, field_name)?;
1102
1103    serialize(&cbor_value)
1104        .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err))
1105}
1106
1107// Encode one `ByKind` field payload into its raw CBOR value form.
1108fn encode_structural_field_cbor_by_kind(
1109    kind: FieldKind,
1110    value: &Value,
1111    field_name: &str,
1112) -> Result<CborValue, InternalError> {
1113    match (kind, value) {
1114        (_, Value::Null) => Ok(CborValue::Null),
1115        (FieldKind::Blob, Value::Blob(value)) => Ok(CborValue::Bytes(value.clone())),
1116        (FieldKind::Bool, Value::Bool(value)) => Ok(CborValue::Bool(*value)),
1117        (FieldKind::Text, Value::Text(value)) => Ok(CborValue::Text(value.clone())),
1118        (FieldKind::Int, Value::Int(value)) => Ok(CborValue::Integer(i128::from(*value))),
1119        (FieldKind::Uint, Value::Uint(value)) => Ok(CborValue::Integer(i128::from(*value))),
1120        (FieldKind::Float32, Value::Float32(value)) => to_cbor_value(value)
1121            .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err)),
1122        (FieldKind::Float64, Value::Float64(value)) => to_cbor_value(value)
1123            .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err)),
1124        (FieldKind::Int128, Value::Int128(value)) => to_cbor_value(value)
1125            .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err)),
1126        (FieldKind::Uint128, Value::Uint128(value)) => to_cbor_value(value)
1127            .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err)),
1128        (FieldKind::Ulid, Value::Ulid(value)) => Ok(CborValue::Text(value.to_string())),
1129        (FieldKind::Account, Value::Account(value)) => encode_leaf_cbor_value(value, field_name),
1130        (FieldKind::Date, Value::Date(value)) => encode_leaf_cbor_value(value, field_name),
1131        (FieldKind::Decimal { .. }, Value::Decimal(value)) => {
1132            encode_leaf_cbor_value(value, field_name)
1133        }
1134        (FieldKind::Duration, Value::Duration(value)) => encode_leaf_cbor_value(value, field_name),
1135        (FieldKind::IntBig, Value::IntBig(value)) => encode_leaf_cbor_value(value, field_name),
1136        (FieldKind::Principal, Value::Principal(value)) => {
1137            encode_leaf_cbor_value(value, field_name)
1138        }
1139        (FieldKind::Subaccount, Value::Subaccount(value)) => {
1140            encode_leaf_cbor_value(value, field_name)
1141        }
1142        (FieldKind::Timestamp, Value::Timestamp(value)) => {
1143            encode_leaf_cbor_value(value, field_name)
1144        }
1145        (FieldKind::UintBig, Value::UintBig(value)) => encode_leaf_cbor_value(value, field_name),
1146        (FieldKind::Unit, Value::Unit) => encode_leaf_cbor_value(&(), field_name),
1147        (FieldKind::Relation { key_kind, .. }, value) => {
1148            encode_structural_field_cbor_by_kind(*key_kind, value, field_name)
1149        }
1150        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1151            Ok(CborValue::Array(
1152                items
1153                    .iter()
1154                    .map(|item| encode_structural_field_cbor_by_kind(*inner, item, field_name))
1155                    .collect::<Result<Vec<_>, _>>()?,
1156            ))
1157        }
1158        (FieldKind::Map { key, value }, Value::Map(entries)) => {
1159            let mut encoded = BTreeMap::new();
1160            for (entry_key, entry_value) in entries {
1161                encoded.insert(
1162                    encode_structural_field_cbor_by_kind(*key, entry_key, field_name)?,
1163                    encode_structural_field_cbor_by_kind(*value, entry_value, field_name)?,
1164                );
1165            }
1166
1167            Ok(CborValue::Map(encoded))
1168        }
1169        (FieldKind::Enum { path, variants }, Value::Enum(value)) => {
1170            encode_enum_cbor_value(path, variants, value, field_name)
1171        }
1172        (FieldKind::Structured { .. }, _) => Err(InternalError::persisted_row_field_encode_failed(
1173            field_name,
1174            "structured ByKind field encoding is unsupported",
1175        )),
1176        _ => Err(InternalError::persisted_row_field_encode_failed(
1177            field_name,
1178            format!("field kind {kind:?} does not accept runtime value {value:?}"),
1179        )),
1180    }
1181}
1182
1183// Encode one typed leaf wrapper into its raw CBOR value form.
1184fn encode_leaf_cbor_value<T>(value: &T, field_name: &str) -> Result<CborValue, InternalError>
1185where
1186    T: serde::Serialize,
1187{
1188    to_cbor_value(value)
1189        .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err))
1190}
1191
1192// Encode one enum field using the same unit-vs-one-entry-map envelope expected
1193// by structural enum decode.
1194fn encode_enum_cbor_value(
1195    path: &'static str,
1196    variants: &'static [crate::model::field::EnumVariantModel],
1197    value: &ValueEnum,
1198    field_name: &str,
1199) -> Result<CborValue, InternalError> {
1200    if let Some(actual_path) = value.path()
1201        && actual_path != path
1202    {
1203        return Err(InternalError::persisted_row_field_encode_failed(
1204            field_name,
1205            format!("enum path mismatch: expected '{path}', found '{actual_path}'"),
1206        ));
1207    }
1208
1209    let Some(payload) = value.payload() else {
1210        return Ok(CborValue::Text(value.variant().to_string()));
1211    };
1212
1213    let Some(variant_model) = variants.iter().find(|item| item.ident() == value.variant()) else {
1214        return Err(InternalError::persisted_row_field_encode_failed(
1215            field_name,
1216            format!(
1217                "unknown enum variant '{}' for path '{path}'",
1218                value.variant()
1219            ),
1220        ));
1221    };
1222    let Some(payload_kind) = variant_model.payload_kind() else {
1223        return Err(InternalError::persisted_row_field_encode_failed(
1224            field_name,
1225            format!(
1226                "enum variant '{}' does not accept a payload",
1227                value.variant()
1228            ),
1229        ));
1230    };
1231
1232    let payload_value = match variant_model.payload_storage_decode() {
1233        FieldStorageDecode::ByKind => {
1234            encode_structural_field_cbor_by_kind(*payload_kind, payload, field_name)?
1235        }
1236        FieldStorageDecode::Value => to_cbor_value(payload)
1237            .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err))?,
1238    };
1239
1240    let mut encoded = BTreeMap::new();
1241    encoded.insert(CborValue::Text(value.variant().to_string()), payload_value);
1242
1243    Ok(CborValue::Map(encoded))
1244}
1245
1246// Resolve one field model entry by stable slot index.
1247fn field_model_for_slot(
1248    model: &'static EntityModel,
1249    slot: usize,
1250) -> Result<&'static FieldModel, InternalError> {
1251    model
1252        .fields()
1253        .get(slot)
1254        .ok_or_else(|| InternalError::persisted_row_slot_lookup_out_of_bounds(model.path(), slot))
1255}
1256
1257///
1258/// SlotBufferWriter
1259///
1260/// SlotBufferWriter captures one dense canonical row worth of slot payloads
1261/// before they are encoded into the canonical slot container.
1262///
1263
1264pub(in crate::db) struct SlotBufferWriter {
1265    model: &'static EntityModel,
1266    slots: Vec<SlotBufferSlot>,
1267}
1268
1269impl SlotBufferWriter {
1270    /// Build one empty slot buffer for one entity model.
1271    pub(in crate::db) fn for_model(model: &'static EntityModel) -> Self {
1272        Self {
1273            model,
1274            slots: vec![SlotBufferSlot::Missing; model.fields().len()],
1275        }
1276    }
1277
1278    /// Encode the buffered slots into the canonical row payload.
1279    pub(in crate::db) fn finish(self) -> Result<Vec<u8>, InternalError> {
1280        let slot_count = self.slots.len();
1281        let mut payload_bytes = Vec::new();
1282        let mut slot_table = Vec::with_capacity(slot_count);
1283
1284        // Phase 1: require one payload for every declared slot before the row
1285        // can cross the canonical persisted-row boundary.
1286        for (slot, slot_payload) in self.slots.into_iter().enumerate() {
1287            match slot_payload {
1288                SlotBufferSlot::Set(bytes) => {
1289                    let start = u32::try_from(payload_bytes.len()).map_err(|_| {
1290                        InternalError::persisted_row_encode_failed(
1291                            "slot payload start exceeds u32 range",
1292                        )
1293                    })?;
1294                    let len = u32::try_from(bytes.len()).map_err(|_| {
1295                        InternalError::persisted_row_encode_failed(
1296                            "slot payload length exceeds u32 range",
1297                        )
1298                    })?;
1299                    payload_bytes.extend_from_slice(&bytes);
1300                    slot_table.push((start, len));
1301                }
1302                SlotBufferSlot::Missing => {
1303                    return Err(InternalError::persisted_row_encode_failed(format!(
1304                        "slot buffer writer did not emit slot {slot} for entity '{}'",
1305                        self.model.path()
1306                    )));
1307                }
1308            }
1309        }
1310
1311        // Phase 2: flatten the slot table plus payload bytes into the canonical row image.
1312        encode_slot_payload_from_parts(slot_count, slot_table.as_slice(), payload_bytes.as_slice())
1313    }
1314}
1315
1316impl SlotWriter for SlotBufferWriter {
1317    fn write_slot(&mut self, slot: usize, payload: Option<&[u8]>) -> Result<(), InternalError> {
1318        let entry = slot_cell_mut(self.slots.as_mut_slice(), slot)?;
1319        let payload = required_slot_payload_bytes(self.model, "slot buffer writer", slot, payload)?;
1320        *entry = SlotBufferSlot::Set(payload.to_vec());
1321
1322        Ok(())
1323    }
1324}
1325
1326///
1327/// SlotBufferSlot
1328///
1329/// SlotBufferSlot tracks whether one canonical row encoder has emitted a
1330/// payload for every declared slot before flattening the row payload.
1331///
1332
1333#[derive(Clone, Debug, Eq, PartialEq)]
1334enum SlotBufferSlot {
1335    Missing,
1336    Set(Vec<u8>),
1337}
1338
1339///
1340/// SerializedPatchWriter
1341///
1342/// SerializedPatchWriter
1343///
1344/// SerializedPatchWriter captures a dense typed entity slot image into the
1345/// serialized patch artifact used by `0.64` mutation staging.
1346/// Unlike `SlotBufferWriter`, this writer does not flatten into one row payload;
1347/// it preserves slot-level ownership so later stages can replay the row through
1348/// the structural patch boundary.
1349///
1350
1351struct SerializedPatchWriter {
1352    model: &'static EntityModel,
1353    slots: Vec<PatchWriterSlot>,
1354}
1355
1356impl SerializedPatchWriter {
1357    /// Build one empty serialized patch writer for one entity model.
1358    fn for_model(model: &'static EntityModel) -> Self {
1359        Self {
1360            model,
1361            slots: vec![PatchWriterSlot::Missing; model.fields().len()],
1362        }
1363    }
1364
1365    /// Materialize one dense serialized patch, erroring if the writer failed
1366    /// to emit any declared slot.
1367    fn finish_complete(self) -> Result<SerializedUpdatePatch, InternalError> {
1368        let mut entries = Vec::with_capacity(self.slots.len());
1369
1370        // Phase 1: require a complete slot image so typed save/update staging
1371        // stays equivalent to the existing full-row encoder.
1372        for (slot, payload) in self.slots.into_iter().enumerate() {
1373            let field_slot = FieldSlot::from_index(self.model, slot)?;
1374            let serialized = match payload {
1375                PatchWriterSlot::Set(payload) => SerializedFieldUpdate::new(field_slot, payload),
1376                PatchWriterSlot::Missing => {
1377                    return Err(InternalError::persisted_row_encode_failed(format!(
1378                        "serialized patch writer did not emit slot {slot} for entity '{}'",
1379                        self.model.path()
1380                    )));
1381                }
1382            };
1383            entries.push(serialized);
1384        }
1385
1386        Ok(SerializedUpdatePatch::new(entries))
1387    }
1388}
1389
1390impl SlotWriter for SerializedPatchWriter {
1391    fn write_slot(&mut self, slot: usize, payload: Option<&[u8]>) -> Result<(), InternalError> {
1392        let entry = slot_cell_mut(self.slots.as_mut_slice(), slot)?;
1393        let payload =
1394            required_slot_payload_bytes(self.model, "serialized patch writer", slot, payload)?;
1395        *entry = PatchWriterSlot::Set(payload.to_vec());
1396
1397        Ok(())
1398    }
1399}
1400
1401///
1402/// PatchWriterSlot
1403///
1404/// PatchWriterSlot
1405///
1406/// PatchWriterSlot tracks whether one dense slot-image writer has emitted a
1407/// payload or failed to visit the slot at all.
1408/// That lets the typed save/update bridge reject incomplete writers instead of
1409/// silently leaving stale bytes in the baseline row.
1410///
1411
1412#[derive(Clone, Debug, Eq, PartialEq)]
1413enum PatchWriterSlot {
1414    Missing,
1415    Set(Vec<u8>),
1416}
1417
1418///
1419/// StructuralSlotReader
1420///
1421/// StructuralSlotReader adapts the current persisted-row bytes into the
1422/// canonical slot-reader seam.
1423/// It validates row shape and every declared field contract before any
1424/// consumer can observe the row, then materializes semantic `Value`s lazily so
1425/// hot readers do not pay full allocation cost for untouched fields.
1426///
1427
1428pub(in crate::db) struct StructuralSlotReader<'a> {
1429    model: &'static EntityModel,
1430    field_bytes: StructuralRowFieldBytes<'a>,
1431    cached_values: Vec<CachedSlotValue>,
1432    #[cfg(any(test, feature = "structural-read-metrics"))]
1433    metrics: StructuralReadProbe,
1434}
1435
1436impl<'a> StructuralSlotReader<'a> {
1437    /// Build one slot reader over one persisted row using the current structural row scanner.
1438    pub(in crate::db) fn from_raw_row(
1439        raw_row: &'a RawRow,
1440        model: &'static EntityModel,
1441    ) -> Result<Self, InternalError> {
1442        let field_bytes = StructuralRowFieldBytes::from_raw_row(raw_row, model)
1443            .map_err(StructuralRowDecodeError::into_internal_error)?;
1444        let cached_values = std::iter::repeat_with(|| CachedSlotValue::Pending)
1445            .take(model.fields().len())
1446            .collect();
1447        let mut reader = Self {
1448            model,
1449            field_bytes,
1450            cached_values,
1451            #[cfg(any(test, feature = "structural-read-metrics"))]
1452            metrics: StructuralReadProbe::begin(model.fields().len()),
1453        };
1454
1455        // Phase 1: validate every declared slot through the field contract
1456        // once so malformed persisted bytes cannot stay latent behind later
1457        // hot-path reads.
1458        reader.validate_all_declared_slots()?;
1459
1460        Ok(reader)
1461    }
1462
1463    /// Validate the decoded primary-key slot against the authoritative row key.
1464    pub(in crate::db) fn validate_storage_key(
1465        &self,
1466        data_key: &DataKey,
1467    ) -> Result<(), InternalError> {
1468        self.validate_storage_key_value(data_key.storage_key())
1469    }
1470
1471    // Validate the decoded primary-key slot against one authoritative storage
1472    // key without rebuilding a full `DataKey` wrapper at the call site.
1473    pub(in crate::db) fn validate_storage_key_value(
1474        &self,
1475        expected_key: StorageKey,
1476    ) -> Result<(), InternalError> {
1477        let Some(primary_key_slot) = resolve_primary_key_slot(self.model) else {
1478            return Err(InternalError::persisted_row_primary_key_field_missing(
1479                self.model.path(),
1480            ));
1481        };
1482        let field = self.field_model(primary_key_slot)?;
1483        let decoded_key = match self.get_scalar(primary_key_slot)? {
1484            Some(ScalarSlotValueRef::Null) => None,
1485            Some(ScalarSlotValueRef::Value(value)) => storage_key_from_scalar_ref(value),
1486            None => Some(
1487                decode_storage_key_field_bytes(
1488                    self.required_field_bytes(primary_key_slot, field.name())?,
1489                    field.kind,
1490                )
1491                .map_err(|err| {
1492                    InternalError::persisted_row_primary_key_not_storage_encodable(
1493                        expected_key,
1494                        err,
1495                    )
1496                })?,
1497            ),
1498        };
1499        let Some(decoded_key) = decoded_key else {
1500            return Err(InternalError::persisted_row_primary_key_slot_missing(
1501                expected_key,
1502            ));
1503        };
1504
1505        if decoded_key != expected_key {
1506            return Err(InternalError::persisted_row_key_mismatch(
1507                expected_key,
1508                decoded_key,
1509            ));
1510        }
1511
1512        Ok(())
1513    }
1514
1515    // Resolve one field model entry by stable slot index.
1516    fn field_model(&self, slot: usize) -> Result<&FieldModel, InternalError> {
1517        field_model_for_slot(self.model, slot)
1518    }
1519
1520    // Validate every declared slot exactly once at the structural row boundary
1521    // so later consumers inherit one globally enforced canonical-row contract
1522    // before any caller can observe the row.
1523    fn validate_all_declared_slots(&mut self) -> Result<(), InternalError> {
1524        for slot in 0..self.model.fields().len() {
1525            self.validate_slot_into_cache(slot)?;
1526        }
1527
1528        Ok(())
1529    }
1530
1531    // Validate one declared slot directly into the owned cache without
1532    // eagerly building the final runtime `Value` unless the slot is already a
1533    // cheap scalar fast-path.
1534    fn validate_slot_into_cache(&mut self, slot: usize) -> Result<(), InternalError> {
1535        if !matches!(self.cached_values.get(slot), Some(CachedSlotValue::Pending)) {
1536            return Ok(());
1537        }
1538
1539        let field = field_model_for_slot(self.model, slot)?;
1540        let raw_value = self
1541            .field_bytes
1542            .field(slot)
1543            .ok_or_else(|| InternalError::persisted_row_declared_field_missing(field.name()))?;
1544        let cached = match field.leaf_codec() {
1545            LeafCodec::Scalar(codec) => CachedSlotValue::Scalar(materialize_scalar_slot_value(
1546                decode_scalar_slot_value(raw_value, codec, field.name())?,
1547            )),
1548            LeafCodec::CborFallback => {
1549                #[cfg(any(test, feature = "structural-read-metrics"))]
1550                self.metrics.record_validated_non_scalar();
1551                validate_non_scalar_slot_value(raw_value, field)?;
1552                CachedSlotValue::Deferred {
1553                    materialized: OnceCell::new(),
1554                }
1555            }
1556        };
1557        self.cached_values[slot] = cached;
1558
1559        Ok(())
1560    }
1561
1562    // Consume the structural slot reader into one slot-indexed decoded-value
1563    // vector once the canonical row boundary has already validated every slot.
1564    // This lets hot row-decode callers pay semantic materialization exactly
1565    // once while preserving the strict row-open fail-closed contract.
1566    pub(in crate::db) fn into_decoded_values(
1567        mut self,
1568    ) -> Result<Vec<Option<Value>>, InternalError> {
1569        let model = self.model;
1570        let cached_values = std::mem::take(&mut self.cached_values);
1571        let mut values = Vec::with_capacity(cached_values.len());
1572
1573        for (slot, cached) in cached_values.into_iter().enumerate() {
1574            match cached {
1575                CachedSlotValue::Scalar(value) => values.push(Some(value)),
1576                CachedSlotValue::Deferred { materialized } => {
1577                    let field = field_model_for_slot(model, slot)?;
1578                    let value = if let Some(value) = materialized.into_inner() {
1579                        value
1580                    } else {
1581                        #[cfg(any(test, feature = "structural-read-metrics"))]
1582                        self.metrics.record_materialized_non_scalar();
1583                        let raw_value = self.field_bytes.field(slot).ok_or_else(|| {
1584                            InternalError::persisted_row_declared_field_missing(field.name())
1585                        })?;
1586                        decode_slot_value_for_field(field, raw_value)?
1587                    };
1588                    values.push(Some(value));
1589                }
1590                CachedSlotValue::Pending => {
1591                    return Err(InternalError::persisted_row_decode_failed(format!(
1592                        "structural slot cache was not fully validated before consumption: slot={slot}",
1593                    )));
1594                }
1595            }
1596        }
1597
1598        Ok(values)
1599    }
1600
1601    // Borrow one declared slot value from the validated structural cache,
1602    // materializing the semantic `Value` lazily when the caller first touches
1603    // that slot.
1604    fn required_cached_value(&self, slot: usize) -> Result<&Value, InternalError> {
1605        let cached = self.cached_values.get(slot).ok_or_else(|| {
1606            InternalError::persisted_row_slot_cache_lookup_out_of_bounds(self.model.path(), slot)
1607        })?;
1608
1609        match cached {
1610            CachedSlotValue::Scalar(value) => Ok(value),
1611            CachedSlotValue::Deferred { materialized } => {
1612                let field = self.field_model(slot)?;
1613                let raw_value = self.required_field_bytes(slot, field.name())?;
1614                if materialized.get().is_none() {
1615                    #[cfg(any(test, feature = "structural-read-metrics"))]
1616                    self.metrics.record_materialized_non_scalar();
1617                    let value = decode_slot_value_for_field(field, raw_value)?;
1618                    let _ = materialized.set(value);
1619                }
1620
1621                materialized.get().ok_or_else(|| {
1622                    InternalError::persisted_row_decode_failed(format!(
1623                        "structural slot cache failed to materialize deferred value: slot={slot}",
1624                    ))
1625                })
1626            }
1627            CachedSlotValue::Pending => Err(InternalError::persisted_row_decode_failed(format!(
1628                "structural slot cache missing validated value after row-open validation: slot={slot}",
1629            ))),
1630        }
1631    }
1632
1633    // Borrow one declared slot payload, treating absence as a persisted-row
1634    // invariant violation instead of a normal structural branch.
1635    pub(in crate::db) fn required_field_bytes(
1636        &self,
1637        slot: usize,
1638        field_name: &str,
1639    ) -> Result<&[u8], InternalError> {
1640        self.field_bytes
1641            .field(slot)
1642            .ok_or_else(|| InternalError::persisted_row_declared_field_missing(field_name))
1643    }
1644}
1645
1646// Convert one scalar slot fast-path value into its storage-key form when the
1647// field kind is storage-key-compatible.
1648const fn storage_key_from_scalar_ref(value: ScalarValueRef<'_>) -> Option<StorageKey> {
1649    match value {
1650        ScalarValueRef::Int(value) => Some(StorageKey::Int(value)),
1651        ScalarValueRef::Principal(value) => Some(StorageKey::Principal(value)),
1652        ScalarValueRef::Subaccount(value) => Some(StorageKey::Subaccount(value)),
1653        ScalarValueRef::Timestamp(value) => Some(StorageKey::Timestamp(value)),
1654        ScalarValueRef::Uint(value) => Some(StorageKey::Uint(value)),
1655        ScalarValueRef::Ulid(value) => Some(StorageKey::Ulid(value)),
1656        ScalarValueRef::Unit => Some(StorageKey::Unit),
1657        _ => None,
1658    }
1659}
1660
1661// Borrow one scalar-slot view directly from one already-decoded runtime value.
1662fn scalar_slot_value_ref_from_cached_value(
1663    value: &Value,
1664) -> Result<ScalarSlotValueRef<'_>, InternalError> {
1665    let scalar = match value {
1666        Value::Null => return Ok(ScalarSlotValueRef::Null),
1667        Value::Blob(value) => ScalarValueRef::Blob(value.as_slice()),
1668        Value::Bool(value) => ScalarValueRef::Bool(*value),
1669        Value::Date(value) => ScalarValueRef::Date(*value),
1670        Value::Duration(value) => ScalarValueRef::Duration(*value),
1671        Value::Float32(value) => ScalarValueRef::Float32(*value),
1672        Value::Float64(value) => ScalarValueRef::Float64(*value),
1673        Value::Int(value) => ScalarValueRef::Int(*value),
1674        Value::Principal(value) => ScalarValueRef::Principal(*value),
1675        Value::Subaccount(value) => ScalarValueRef::Subaccount(*value),
1676        Value::Text(value) => ScalarValueRef::Text(value.as_str()),
1677        Value::Timestamp(value) => ScalarValueRef::Timestamp(*value),
1678        Value::Uint(value) => ScalarValueRef::Uint(*value),
1679        Value::Ulid(value) => ScalarValueRef::Ulid(*value),
1680        Value::Unit => ScalarValueRef::Unit,
1681        _ => {
1682            return Err(InternalError::persisted_row_decode_failed(format!(
1683                "cached structural scalar slot cannot borrow non-scalar value variant: {value:?}",
1684            )));
1685        }
1686    };
1687
1688    Ok(ScalarSlotValueRef::Value(scalar))
1689}
1690
1691// Materialize one validated scalar slot view into the runtime `Value` enum.
1692fn materialize_scalar_slot_value(value: ScalarSlotValueRef<'_>) -> Value {
1693    match value {
1694        ScalarSlotValueRef::Null => Value::Null,
1695        ScalarSlotValueRef::Value(value) => value.into_value(),
1696    }
1697}
1698
1699impl SlotReader for StructuralSlotReader<'_> {
1700    fn model(&self) -> &'static EntityModel {
1701        self.model
1702    }
1703
1704    fn has(&self, slot: usize) -> bool {
1705        self.field_bytes.field(slot).is_some()
1706    }
1707
1708    fn get_bytes(&self, slot: usize) -> Option<&[u8]> {
1709        self.field_bytes.field(slot)
1710    }
1711
1712    fn get_scalar(&self, slot: usize) -> Result<Option<ScalarSlotValueRef<'_>>, InternalError> {
1713        let field = self.field_model(slot)?;
1714
1715        match field.leaf_codec() {
1716            LeafCodec::Scalar(_codec) => match self.cached_values.get(slot) {
1717                Some(CachedSlotValue::Scalar(value)) => {
1718                    scalar_slot_value_ref_from_cached_value(value).map(Some)
1719                }
1720                Some(CachedSlotValue::Pending) => {
1721                    Err(InternalError::persisted_row_decode_failed(format!(
1722                        "structural scalar slot cache missing validated value after row-open validation: slot={slot}",
1723                    )))
1724                }
1725                Some(CachedSlotValue::Deferred { .. }) => {
1726                    Err(InternalError::persisted_row_decode_failed(format!(
1727                        "structural scalar slot routed through non-scalar cache variant: slot={slot}",
1728                    )))
1729                }
1730                None => Err(
1731                    InternalError::persisted_row_slot_cache_lookup_out_of_bounds(
1732                        self.model.path(),
1733                        slot,
1734                    ),
1735                ),
1736            },
1737            LeafCodec::CborFallback => Ok(None),
1738        }
1739    }
1740
1741    fn get_value(&mut self, slot: usize) -> Result<Option<Value>, InternalError> {
1742        self.validate_slot_into_cache(slot)?;
1743        Ok(Some(self.required_cached_value(slot)?.clone()))
1744    }
1745}
1746
1747impl CanonicalSlotReader for StructuralSlotReader<'_> {
1748    fn required_value_by_contract(&self, slot: usize) -> Result<Value, InternalError> {
1749        Ok(self.required_cached_value(slot)?.clone())
1750    }
1751
1752    fn required_value_by_contract_cow(&self, slot: usize) -> Result<Cow<'_, Value>, InternalError> {
1753        Ok(Cow::Borrowed(self.required_cached_value(slot)?))
1754    }
1755}
1756
1757///
1758/// CachedSlotValue
1759///
1760/// CachedSlotValue tracks whether one slot has already been validated, and
1761/// whether its semantic runtime `Value` has been materialized yet, during the
1762/// current structural row access pass.
1763///
1764
1765#[derive(Debug)]
1766enum CachedSlotValue {
1767    Pending,
1768    Scalar(Value),
1769    Deferred { materialized: OnceCell<Value> },
1770}
1771
1772///
1773/// StructuralReadMetrics
1774///
1775/// StructuralReadMetrics aggregates one test-scoped view of structural row
1776/// validation and lazy non-scalar materialization activity.
1777/// It lets row-backed benchmarks prove the new boundary validates all declared
1778/// slots while only materializing the non-scalar slots a caller actually
1779/// touches.
1780///
1781
1782#[cfg(any(test, feature = "structural-read-metrics"))]
1783#[cfg_attr(
1784    all(test, not(feature = "structural-read-metrics")),
1785    allow(unreachable_pub)
1786)]
1787#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1788pub struct StructuralReadMetrics {
1789    pub rows_opened: u64,
1790    pub declared_slots_validated: u64,
1791    pub validated_non_scalar_slots: u64,
1792    pub materialized_non_scalar_slots: u64,
1793    pub rows_without_lazy_non_scalar_materializations: u64,
1794}
1795
1796#[cfg(any(test, feature = "structural-read-metrics"))]
1797std::thread_local! {
1798    static STRUCTURAL_READ_METRICS: RefCell<Option<StructuralReadMetrics>> = const {
1799        RefCell::new(None)
1800    };
1801}
1802
1803///
1804/// StructuralReadProbe
1805///
1806/// StructuralReadProbe tracks one reader instance's structural validation and
1807/// deferred non-scalar materialization counts while a test-scoped metrics
1808/// capture is active.
1809///
1810
1811#[cfg(any(test, feature = "structural-read-metrics"))]
1812#[derive(Debug)]
1813struct StructuralReadProbe {
1814    collect: bool,
1815    declared_slots_validated: u64,
1816    validated_non_scalar_slots: Cell<u64>,
1817    materialized_non_scalar_slots: Cell<u64>,
1818}
1819
1820#[cfg(any(test, feature = "structural-read-metrics"))]
1821impl StructuralReadProbe {
1822    // Begin one optional per-reader metrics probe when a test-scoped capture
1823    // is active on the current thread.
1824    fn begin(field_count: usize) -> Self {
1825        let collect = STRUCTURAL_READ_METRICS.with(|metrics| metrics.borrow().is_some());
1826
1827        Self {
1828            collect,
1829            declared_slots_validated: field_count as u64,
1830            validated_non_scalar_slots: Cell::new(0),
1831            materialized_non_scalar_slots: Cell::new(0),
1832        }
1833    }
1834
1835    // Record one non-scalar slot validated at row-open.
1836    fn record_validated_non_scalar(&self) {
1837        if !self.collect {
1838            return;
1839        }
1840
1841        self.validated_non_scalar_slots
1842            .set(self.validated_non_scalar_slots.get().saturating_add(1));
1843    }
1844
1845    // Record one distinct non-scalar slot materialized after row-open.
1846    fn record_materialized_non_scalar(&self) {
1847        if !self.collect {
1848            return;
1849        }
1850
1851        self.materialized_non_scalar_slots
1852            .set(self.materialized_non_scalar_slots.get().saturating_add(1));
1853    }
1854}
1855
1856#[cfg(any(test, feature = "structural-read-metrics"))]
1857impl Drop for StructuralSlotReader<'_> {
1858    fn drop(&mut self) {
1859        if !self.metrics.collect {
1860            return;
1861        }
1862
1863        let validated_non_scalar_slots = self.metrics.validated_non_scalar_slots.get();
1864        let materialized_non_scalar_slots = self.metrics.materialized_non_scalar_slots.get();
1865
1866        STRUCTURAL_READ_METRICS.with(|metrics| {
1867            if let Some(aggregate) = metrics.borrow_mut().as_mut() {
1868                aggregate.rows_opened = aggregate.rows_opened.saturating_add(1);
1869                aggregate.declared_slots_validated = aggregate
1870                    .declared_slots_validated
1871                    .saturating_add(self.metrics.declared_slots_validated);
1872                aggregate.validated_non_scalar_slots = aggregate
1873                    .validated_non_scalar_slots
1874                    .saturating_add(validated_non_scalar_slots);
1875                aggregate.materialized_non_scalar_slots = aggregate
1876                    .materialized_non_scalar_slots
1877                    .saturating_add(materialized_non_scalar_slots);
1878                if materialized_non_scalar_slots == 0 {
1879                    aggregate.rows_without_lazy_non_scalar_materializations = aggregate
1880                        .rows_without_lazy_non_scalar_materializations
1881                        .saturating_add(1);
1882                }
1883            }
1884        });
1885    }
1886}
1887
1888///
1889/// with_structural_read_metrics
1890///
1891/// Run one closure while collecting structural-read metrics on the current
1892/// thread, then return the closure result plus the aggregated snapshot.
1893///
1894
1895#[cfg(any(test, feature = "structural-read-metrics"))]
1896#[cfg_attr(
1897    all(test, not(feature = "structural-read-metrics")),
1898    allow(unreachable_pub)
1899)]
1900pub fn with_structural_read_metrics<T>(f: impl FnOnce() -> T) -> (T, StructuralReadMetrics) {
1901    STRUCTURAL_READ_METRICS.with(|metrics| {
1902        debug_assert!(
1903            metrics.borrow().is_none(),
1904            "structural read metrics captures should not nest"
1905        );
1906        *metrics.borrow_mut() = Some(StructuralReadMetrics::default());
1907    });
1908
1909    let result = f();
1910    let metrics =
1911        STRUCTURAL_READ_METRICS.with(|metrics| metrics.borrow_mut().take().unwrap_or_default());
1912
1913    (result, metrics)
1914}
1915
1916///
1917/// TESTS
1918///
1919
1920#[cfg(test)]
1921mod tests {
1922    use super::{
1923        CachedSlotValue, FieldSlot, ScalarSlotValueRef, ScalarValueRef, SerializedFieldUpdate,
1924        SerializedPatchWriter, SerializedUpdatePatch, SlotBufferWriter, SlotReader, SlotWriter,
1925        UpdatePatch, apply_serialized_update_patch_to_raw_row, apply_update_patch_to_raw_row,
1926        decode_persisted_custom_many_slot_payload, decode_persisted_custom_slot_payload,
1927        decode_persisted_non_null_slot_payload, decode_persisted_option_slot_payload,
1928        decode_persisted_slot_payload, decode_slot_value_by_contract, decode_slot_value_from_bytes,
1929        encode_persisted_custom_many_slot_payload, encode_persisted_custom_slot_payload,
1930        encode_scalar_slot_value, encode_slot_payload_from_parts, encode_slot_value_from_value,
1931        serialize_entity_slots_as_update_patch, serialize_update_patch_fields,
1932        with_structural_read_metrics,
1933    };
1934    use crate::{
1935        db::{
1936            codec::serialize_row_payload,
1937            data::{RawRow, StructuralSlotReader, decode_structural_value_storage_bytes},
1938        },
1939        error::InternalError,
1940        model::{
1941            EntityModel,
1942            field::{EnumVariantModel, FieldKind, FieldModel, FieldStorageDecode},
1943        },
1944        serialize::serialize,
1945        testing::SIMPLE_ENTITY_TAG,
1946        traits::{EntitySchema, FieldValue},
1947        types::{
1948            Account, Date, Decimal, Duration, Float32, Float64, Int, Int128, Nat, Nat128,
1949            Principal, Subaccount, Timestamp, Ulid,
1950        },
1951        value::{Value, ValueEnum},
1952    };
1953    use icydb_derive::{FieldProjection, PersistedRow};
1954    use serde::{Deserialize, Serialize};
1955
1956    crate::test_canister! {
1957        ident = PersistedRowPatchBridgeCanister,
1958        commit_memory_id = crate::testing::test_commit_memory_id(),
1959    }
1960
1961    crate::test_store! {
1962        ident = PersistedRowPatchBridgeStore,
1963        canister = PersistedRowPatchBridgeCanister,
1964    }
1965
1966    ///
1967    /// PersistedRowPatchBridgeEntity
1968    ///
1969    /// PersistedRowPatchBridgeEntity
1970    ///
1971    /// PersistedRowPatchBridgeEntity is the smallest derive-owned entity used
1972    /// to validate the typed-entity -> serialized-patch bridge.
1973    /// It lets the persisted-row tests exercise the same dense slot writer the
1974    /// save/update path now uses.
1975    ///
1976
1977    #[derive(
1978        Clone, Debug, Default, Deserialize, FieldProjection, PartialEq, PersistedRow, Serialize,
1979    )]
1980    struct PersistedRowPatchBridgeEntity {
1981        id: crate::types::Ulid,
1982        name: String,
1983    }
1984
1985    #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1986    struct PersistedRowProfileValue {
1987        bio: String,
1988    }
1989
1990    impl FieldValue for PersistedRowProfileValue {
1991        fn kind() -> crate::traits::FieldValueKind {
1992            crate::traits::FieldValueKind::Structured { queryable: false }
1993        }
1994
1995        fn to_value(&self) -> Value {
1996            Value::from_map(vec![(
1997                Value::Text("bio".to_string()),
1998                Value::Text(self.bio.clone()),
1999            )])
2000            .expect("profile test value should encode as canonical map")
2001        }
2002
2003        fn from_value(value: &Value) -> Option<Self> {
2004            let Value::Map(entries) = value else {
2005                return None;
2006            };
2007            let normalized = Value::normalize_map_entries(entries.clone()).ok()?;
2008            let bio = normalized
2009                .iter()
2010                .find_map(|(entry_key, entry_value)| match entry_key {
2011                    Value::Text(entry_key) if entry_key == "bio" => match entry_value {
2012                        Value::Text(bio) => Some(bio.clone()),
2013                        _ => None,
2014                    },
2015                    _ => None,
2016                })?;
2017
2018            if normalized.len() != 1 {
2019                return None;
2020            }
2021
2022            Some(Self { bio })
2023        }
2024    }
2025
2026    crate::test_entity_schema! {
2027        ident = PersistedRowPatchBridgeEntity,
2028        id = crate::types::Ulid,
2029        id_field = id,
2030        entity_name = "PersistedRowPatchBridgeEntity",
2031        entity_tag = SIMPLE_ENTITY_TAG,
2032        pk_index = 0,
2033        fields = [
2034            ("id", FieldKind::Ulid),
2035            ("name", FieldKind::Text),
2036        ],
2037        indexes = [],
2038        store = PersistedRowPatchBridgeStore,
2039        canister = PersistedRowPatchBridgeCanister,
2040    }
2041
2042    static STATE_VARIANTS: &[EnumVariantModel] = &[EnumVariantModel::new(
2043        "Loaded",
2044        Some(&FieldKind::Uint),
2045        FieldStorageDecode::ByKind,
2046    )];
2047    static FIELD_MODELS: [FieldModel; 2] = [
2048        FieldModel::new("name", FieldKind::Text),
2049        FieldModel::new_with_storage_decode("payload", FieldKind::Text, FieldStorageDecode::Value),
2050    ];
2051    static LIST_FIELD_MODELS: [FieldModel; 1] =
2052        [FieldModel::new("tags", FieldKind::List(&FieldKind::Text))];
2053    static MAP_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
2054        "props",
2055        FieldKind::Map {
2056            key: &FieldKind::Text,
2057            value: &FieldKind::Uint,
2058        },
2059    )];
2060    static ENUM_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
2061        "state",
2062        FieldKind::Enum {
2063            path: "tests::State",
2064            variants: STATE_VARIANTS,
2065        },
2066    )];
2067    static ACCOUNT_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new("owner", FieldKind::Account)];
2068    static OPTIONAL_ACCOUNT_FIELD_MODELS: [FieldModel; 1] =
2069        [FieldModel::new_with_storage_decode_and_nullability(
2070            "from",
2071            FieldKind::Account,
2072            FieldStorageDecode::ByKind,
2073            true,
2074        )];
2075    static REQUIRED_STRUCTURED_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
2076        "profile",
2077        FieldKind::Structured { queryable: false },
2078    )];
2079    static OPTIONAL_STRUCTURED_FIELD_MODELS: [FieldModel; 1] =
2080        [FieldModel::new_with_storage_decode_and_nullability(
2081            "profile",
2082            FieldKind::Structured { queryable: false },
2083            FieldStorageDecode::ByKind,
2084            true,
2085        )];
2086    static VALUE_STORAGE_STRUCTURED_FIELD_MODELS: [FieldModel; 1] =
2087        [FieldModel::new_with_storage_decode(
2088            "manifest",
2089            FieldKind::Structured { queryable: false },
2090            FieldStorageDecode::Value,
2091        )];
2092    static STRUCTURED_MAP_VALUE_KIND: FieldKind = FieldKind::Structured { queryable: false };
2093    static STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS: [FieldModel; 1] =
2094        [FieldModel::new_with_storage_decode(
2095            "projects",
2096            FieldKind::Map {
2097                key: &FieldKind::Principal,
2098                value: &STRUCTURED_MAP_VALUE_KIND,
2099            },
2100            FieldStorageDecode::Value,
2101        )];
2102    static INDEX_MODELS: [&crate::model::index::IndexModel; 0] = [];
2103    static TEST_MODEL: EntityModel = EntityModel::new(
2104        "tests::PersistedRowFieldCodecEntity",
2105        "persisted_row_field_codec_entity",
2106        &FIELD_MODELS[0],
2107        &FIELD_MODELS,
2108        &INDEX_MODELS,
2109    );
2110    static LIST_MODEL: EntityModel = EntityModel::new(
2111        "tests::PersistedRowListFieldCodecEntity",
2112        "persisted_row_list_field_codec_entity",
2113        &LIST_FIELD_MODELS[0],
2114        &LIST_FIELD_MODELS,
2115        &INDEX_MODELS,
2116    );
2117    static MAP_MODEL: EntityModel = EntityModel::new(
2118        "tests::PersistedRowMapFieldCodecEntity",
2119        "persisted_row_map_field_codec_entity",
2120        &MAP_FIELD_MODELS[0],
2121        &MAP_FIELD_MODELS,
2122        &INDEX_MODELS,
2123    );
2124    static ENUM_MODEL: EntityModel = EntityModel::new(
2125        "tests::PersistedRowEnumFieldCodecEntity",
2126        "persisted_row_enum_field_codec_entity",
2127        &ENUM_FIELD_MODELS[0],
2128        &ENUM_FIELD_MODELS,
2129        &INDEX_MODELS,
2130    );
2131    static ACCOUNT_MODEL: EntityModel = EntityModel::new(
2132        "tests::PersistedRowAccountFieldCodecEntity",
2133        "persisted_row_account_field_codec_entity",
2134        &ACCOUNT_FIELD_MODELS[0],
2135        &ACCOUNT_FIELD_MODELS,
2136        &INDEX_MODELS,
2137    );
2138    static OPTIONAL_ACCOUNT_MODEL: EntityModel = EntityModel::new(
2139        "tests::PersistedRowOptionalAccountFieldCodecEntity",
2140        "persisted_row_optional_account_field_codec_entity",
2141        &OPTIONAL_ACCOUNT_FIELD_MODELS[0],
2142        &OPTIONAL_ACCOUNT_FIELD_MODELS,
2143        &INDEX_MODELS,
2144    );
2145    static REQUIRED_STRUCTURED_MODEL: EntityModel = EntityModel::new(
2146        "tests::PersistedRowRequiredStructuredFieldCodecEntity",
2147        "persisted_row_required_structured_field_codec_entity",
2148        &REQUIRED_STRUCTURED_FIELD_MODELS[0],
2149        &REQUIRED_STRUCTURED_FIELD_MODELS,
2150        &INDEX_MODELS,
2151    );
2152    static OPTIONAL_STRUCTURED_MODEL: EntityModel = EntityModel::new(
2153        "tests::PersistedRowOptionalStructuredFieldCodecEntity",
2154        "persisted_row_optional_structured_field_codec_entity",
2155        &OPTIONAL_STRUCTURED_FIELD_MODELS[0],
2156        &OPTIONAL_STRUCTURED_FIELD_MODELS,
2157        &INDEX_MODELS,
2158    );
2159    static VALUE_STORAGE_STRUCTURED_MODEL: EntityModel = EntityModel::new(
2160        "tests::PersistedRowValueStorageStructuredFieldCodecEntity",
2161        "persisted_row_value_storage_structured_field_codec_entity",
2162        &VALUE_STORAGE_STRUCTURED_FIELD_MODELS[0],
2163        &VALUE_STORAGE_STRUCTURED_FIELD_MODELS,
2164        &INDEX_MODELS,
2165    );
2166    static STRUCTURED_MAP_VALUE_STORAGE_MODEL: EntityModel = EntityModel::new(
2167        "tests::PersistedRowStructuredMapValueStorageEntity",
2168        "persisted_row_structured_map_value_storage_entity",
2169        &STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS[0],
2170        &STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS,
2171        &INDEX_MODELS,
2172    );
2173
2174    fn representative_value_storage_cases() -> Vec<Value> {
2175        let nested = Value::from_map(vec![
2176            (
2177                Value::Text("blob".to_string()),
2178                Value::Blob(vec![0x10, 0x20, 0x30]),
2179            ),
2180            (
2181                Value::Text("i128".to_string()),
2182                Value::Int128(Int128::from(-123i128)),
2183            ),
2184            (
2185                Value::Text("u128".to_string()),
2186                Value::Uint128(Nat128::from(456u128)),
2187            ),
2188            (
2189                Value::Text("enum".to_string()),
2190                Value::Enum(
2191                    ValueEnum::new("Loaded", Some("tests::PersistedRowManifest"))
2192                        .with_payload(Value::Blob(vec![0xAA, 0xBB])),
2193                ),
2194            ),
2195        ])
2196        .expect("nested value storage case should normalize");
2197
2198        vec![
2199            Value::Account(Account::dummy(7)),
2200            Value::Blob(vec![1u8, 2u8, 3u8]),
2201            Value::Bool(true),
2202            Value::Date(Date::new(2024, 1, 2)),
2203            Value::Decimal(Decimal::new(123, 2)),
2204            Value::Duration(Duration::from_secs(1)),
2205            Value::Enum(
2206                ValueEnum::new("Ready", Some("tests::PersistedRowState"))
2207                    .with_payload(nested.clone()),
2208            ),
2209            Value::Float32(Float32::try_new(1.25).expect("float32 sample should be finite")),
2210            Value::Float64(Float64::try_new(2.5).expect("float64 sample should be finite")),
2211            Value::Int(-7),
2212            Value::Int128(Int128::from(123i128)),
2213            Value::IntBig(Int::from(99i32)),
2214            Value::List(vec![
2215                Value::Blob(vec![0xCC, 0xDD]),
2216                Value::Text("nested".to_string()),
2217                nested.clone(),
2218            ]),
2219            nested,
2220            Value::Null,
2221            Value::Principal(Principal::dummy(9)),
2222            Value::Subaccount(Subaccount::new([7u8; 32])),
2223            Value::Text("example".to_string()),
2224            Value::Timestamp(Timestamp::from_secs(1)),
2225            Value::Uint(7),
2226            Value::Uint128(Nat128::from(9u128)),
2227            Value::UintBig(Nat::from(11u64)),
2228            Value::Ulid(Ulid::from_u128(42)),
2229            Value::Unit,
2230        ]
2231    }
2232
2233    fn representative_structured_value_storage_cases() -> Vec<Value> {
2234        let nested_map = Value::from_map(vec![
2235            (
2236                Value::Text("account".to_string()),
2237                Value::Account(Account::dummy(7)),
2238            ),
2239            (
2240                Value::Text("blob".to_string()),
2241                Value::Blob(vec![1u8, 2u8, 3u8]),
2242            ),
2243            (Value::Text("bool".to_string()), Value::Bool(true)),
2244            (
2245                Value::Text("date".to_string()),
2246                Value::Date(Date::new(2024, 1, 2)),
2247            ),
2248            (
2249                Value::Text("decimal".to_string()),
2250                Value::Decimal(Decimal::new(123, 2)),
2251            ),
2252            (
2253                Value::Text("duration".to_string()),
2254                Value::Duration(Duration::from_secs(1)),
2255            ),
2256            (
2257                Value::Text("enum".to_string()),
2258                Value::Enum(
2259                    ValueEnum::new("Loaded", Some("tests::PersistedRowManifest"))
2260                        .with_payload(Value::Blob(vec![0xAA, 0xBB])),
2261                ),
2262            ),
2263            (
2264                Value::Text("f32".to_string()),
2265                Value::Float32(Float32::try_new(1.25).expect("float32 sample should be finite")),
2266            ),
2267            (
2268                Value::Text("f64".to_string()),
2269                Value::Float64(Float64::try_new(2.5).expect("float64 sample should be finite")),
2270            ),
2271            (Value::Text("i64".to_string()), Value::Int(-7)),
2272            (
2273                Value::Text("i128".to_string()),
2274                Value::Int128(Int128::from(123i128)),
2275            ),
2276            (
2277                Value::Text("ibig".to_string()),
2278                Value::IntBig(Int::from(99i32)),
2279            ),
2280            (Value::Text("null".to_string()), Value::Null),
2281            (
2282                Value::Text("principal".to_string()),
2283                Value::Principal(Principal::dummy(9)),
2284            ),
2285            (
2286                Value::Text("subaccount".to_string()),
2287                Value::Subaccount(Subaccount::new([7u8; 32])),
2288            ),
2289            (
2290                Value::Text("text".to_string()),
2291                Value::Text("example".to_string()),
2292            ),
2293            (
2294                Value::Text("timestamp".to_string()),
2295                Value::Timestamp(Timestamp::from_secs(1)),
2296            ),
2297            (Value::Text("u64".to_string()), Value::Uint(7)),
2298            (
2299                Value::Text("u128".to_string()),
2300                Value::Uint128(Nat128::from(9u128)),
2301            ),
2302            (
2303                Value::Text("ubig".to_string()),
2304                Value::UintBig(Nat::from(11u64)),
2305            ),
2306            (
2307                Value::Text("ulid".to_string()),
2308                Value::Ulid(Ulid::from_u128(42)),
2309            ),
2310            (Value::Text("unit".to_string()), Value::Unit),
2311        ])
2312        .expect("structured value-storage map should normalize");
2313
2314        vec![
2315            nested_map.clone(),
2316            Value::List(vec![
2317                Value::Blob(vec![0xCC, 0xDD]),
2318                Value::Text("nested".to_string()),
2319                nested_map,
2320            ]),
2321        ]
2322    }
2323
2324    fn encode_slot_payload_allowing_missing_for_tests(
2325        model: &'static EntityModel,
2326        slots: &[Option<&[u8]>],
2327    ) -> Result<Vec<u8>, InternalError> {
2328        if slots.len() != model.fields().len() {
2329            return Err(InternalError::persisted_row_encode_failed(format!(
2330                "noncanonical slot payload test helper expected {} slots for entity '{}', found {}",
2331                model.fields().len(),
2332                model.path(),
2333                slots.len()
2334            )));
2335        }
2336        let mut payload_bytes = Vec::new();
2337        let mut slot_table = Vec::with_capacity(slots.len());
2338
2339        for slot_payload in slots {
2340            match slot_payload {
2341                Some(bytes) => {
2342                    let start = u32::try_from(payload_bytes.len()).map_err(|_| {
2343                        InternalError::persisted_row_encode_failed(
2344                            "slot payload start exceeds u32 range",
2345                        )
2346                    })?;
2347                    let len = u32::try_from(bytes.len()).map_err(|_| {
2348                        InternalError::persisted_row_encode_failed(
2349                            "slot payload length exceeds u32 range",
2350                        )
2351                    })?;
2352                    payload_bytes.extend_from_slice(bytes);
2353                    slot_table.push((start, len));
2354                }
2355                None => slot_table.push((0_u32, 0_u32)),
2356            }
2357        }
2358
2359        encode_slot_payload_from_parts(slots.len(), slot_table.as_slice(), payload_bytes.as_slice())
2360    }
2361
2362    #[test]
2363    fn decode_slot_value_from_bytes_decodes_scalar_slots_through_one_owner() {
2364        let payload =
2365            encode_scalar_slot_value(ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")));
2366        let value =
2367            decode_slot_value_from_bytes(&TEST_MODEL, 0, payload.as_slice()).expect("decode slot");
2368
2369        assert_eq!(value, Value::Text("Ada".to_string()));
2370    }
2371
2372    #[test]
2373    fn decode_slot_value_from_bytes_reports_scalar_prefix_bytes() {
2374        let err = decode_slot_value_from_bytes(&TEST_MODEL, 0, &[0x00, 1])
2375            .expect_err("invalid scalar slot prefix should fail closed");
2376
2377        assert!(
2378            err.message
2379                .contains("expected slot envelope prefix byte 0xFF, found 0x00"),
2380            "unexpected error: {err:?}"
2381        );
2382    }
2383
2384    #[test]
2385    fn decode_slot_value_from_bytes_respects_value_storage_decode_contract() {
2386        let payload = crate::serialize::serialize(&Value::Text("Ada".to_string()))
2387            .expect("encode value-storage payload");
2388
2389        let value =
2390            decode_slot_value_from_bytes(&TEST_MODEL, 1, payload.as_slice()).expect("decode slot");
2391
2392        assert_eq!(value, Value::Text("Ada".to_string()));
2393    }
2394
2395    #[test]
2396    fn encode_slot_value_from_value_roundtrips_scalar_slots() {
2397        let payload = encode_slot_value_from_value(&TEST_MODEL, 0, &Value::Text("Ada".to_string()))
2398            .expect("encode slot");
2399        let decoded =
2400            decode_slot_value_from_bytes(&TEST_MODEL, 0, payload.as_slice()).expect("decode slot");
2401
2402        assert_eq!(decoded, Value::Text("Ada".to_string()));
2403    }
2404
2405    #[test]
2406    fn encode_slot_value_from_value_roundtrips_value_storage_slots() {
2407        let payload = encode_slot_value_from_value(&TEST_MODEL, 1, &Value::Text("Ada".to_string()))
2408            .expect("encode slot");
2409        let decoded =
2410            decode_slot_value_from_bytes(&TEST_MODEL, 1, payload.as_slice()).expect("decode slot");
2411
2412        assert_eq!(decoded, Value::Text("Ada".to_string()));
2413    }
2414
2415    #[test]
2416    fn encode_slot_value_from_value_roundtrips_structured_value_storage_slots_for_all_cases() {
2417        for value in representative_structured_value_storage_cases() {
2418            let payload = encode_slot_value_from_value(&VALUE_STORAGE_STRUCTURED_MODEL, 0, &value)
2419                .unwrap_or_else(|err| {
2420                    panic!(
2421                        "structured value-storage slot should encode for value {value:?}: {err:?}"
2422                    )
2423                });
2424            let decoded = decode_slot_value_from_bytes(
2425                &VALUE_STORAGE_STRUCTURED_MODEL,
2426                0,
2427                payload.as_slice(),
2428            )
2429            .unwrap_or_else(|err| {
2430                panic!(
2431                    "structured value-storage slot should decode for value {value:?} with payload {payload:?}: {err:?}"
2432                )
2433            });
2434
2435            assert_eq!(decoded, value);
2436        }
2437    }
2438
2439    #[test]
2440    fn encode_slot_value_from_value_roundtrips_list_by_kind_slots() {
2441        let payload = encode_slot_value_from_value(
2442            &LIST_MODEL,
2443            0,
2444            &Value::List(vec![Value::Text("alpha".to_string())]),
2445        )
2446        .expect("encode list slot");
2447        let decoded =
2448            decode_slot_value_from_bytes(&LIST_MODEL, 0, payload.as_slice()).expect("decode slot");
2449
2450        assert_eq!(decoded, Value::List(vec![Value::Text("alpha".to_string())]),);
2451    }
2452
2453    #[test]
2454    fn encode_slot_value_from_value_roundtrips_map_by_kind_slots() {
2455        let payload = encode_slot_value_from_value(
2456            &MAP_MODEL,
2457            0,
2458            &Value::Map(vec![(Value::Text("alpha".to_string()), Value::Uint(7))]),
2459        )
2460        .expect("encode map slot");
2461        let decoded =
2462            decode_slot_value_from_bytes(&MAP_MODEL, 0, payload.as_slice()).expect("decode slot");
2463
2464        assert_eq!(
2465            decoded,
2466            Value::Map(vec![(Value::Text("alpha".to_string()), Value::Uint(7))]),
2467        );
2468    }
2469
2470    #[test]
2471    fn encode_slot_value_from_value_accepts_value_storage_maps_with_structured_values() {
2472        let principal = Principal::dummy(7);
2473        let project = Value::from_map(vec![
2474            (Value::Text("pid".to_string()), Value::Principal(principal)),
2475            (
2476                Value::Text("status".to_string()),
2477                Value::Enum(ValueEnum::new(
2478                    "Saved",
2479                    Some("design::app::user::customise::project::ProjectStatus"),
2480                )),
2481            ),
2482        ])
2483        .expect("project value should normalize into a canonical map");
2484        let projects = Value::from_map(vec![(Value::Principal(principal), project)])
2485            .expect("outer map should normalize into a canonical map");
2486
2487        let payload =
2488            encode_slot_value_from_value(&STRUCTURED_MAP_VALUE_STORAGE_MODEL, 0, &projects)
2489                .expect("encode structured map slot");
2490        let decoded = decode_slot_value_from_bytes(
2491            &STRUCTURED_MAP_VALUE_STORAGE_MODEL,
2492            0,
2493            payload.as_slice(),
2494        )
2495        .expect("decode structured map slot");
2496
2497        assert_eq!(decoded, projects);
2498    }
2499
2500    #[test]
2501    fn structured_value_storage_cases_decode_through_direct_value_storage_boundary() {
2502        for value in representative_value_storage_cases() {
2503            let payload = serialize(&value).unwrap_or_else(|err| {
2504                panic!(
2505                    "structured value-storage payload should serialize for value {value:?}: {err:?}"
2506                )
2507            });
2508            let decoded = decode_structural_value_storage_bytes(payload.as_slice()).unwrap_or_else(
2509                |err| {
2510                    panic!(
2511                        "structured value-storage payload should decode for value {value:?} with payload {payload:?}: {err:?}"
2512                    )
2513                },
2514            );
2515
2516            assert_eq!(decoded, value);
2517        }
2518    }
2519
2520    #[test]
2521    fn encode_slot_value_from_value_roundtrips_enum_by_kind_slots() {
2522        let payload = encode_slot_value_from_value(
2523            &ENUM_MODEL,
2524            0,
2525            &Value::Enum(
2526                ValueEnum::new("Loaded", Some("tests::State")).with_payload(Value::Uint(7)),
2527            ),
2528        )
2529        .expect("encode enum slot");
2530        let decoded =
2531            decode_slot_value_from_bytes(&ENUM_MODEL, 0, payload.as_slice()).expect("decode slot");
2532
2533        assert_eq!(
2534            decoded,
2535            Value::Enum(
2536                ValueEnum::new("Loaded", Some("tests::State")).with_payload(Value::Uint(7,))
2537            ),
2538        );
2539    }
2540
2541    #[test]
2542    fn encode_slot_value_from_value_roundtrips_leaf_by_kind_wrapper_slots() {
2543        let account = Account::from_parts(Principal::dummy(7), Some(Subaccount::from([7_u8; 32])));
2544        let payload = encode_slot_value_from_value(&ACCOUNT_MODEL, 0, &Value::Account(account))
2545            .expect("encode account slot");
2546        let decoded = decode_slot_value_from_bytes(&ACCOUNT_MODEL, 0, payload.as_slice())
2547            .expect("decode slot");
2548
2549        assert_eq!(decoded, Value::Account(account));
2550    }
2551
2552    #[test]
2553    fn custom_slot_payload_roundtrips_structured_field_value() {
2554        let profile = PersistedRowProfileValue {
2555            bio: "Ada".to_string(),
2556        };
2557        let payload = encode_persisted_custom_slot_payload(&profile, "profile")
2558            .expect("encode custom structured payload");
2559        let decoded = decode_persisted_custom_slot_payload::<PersistedRowProfileValue>(
2560            payload.as_slice(),
2561            "profile",
2562        )
2563        .expect("decode custom structured payload");
2564
2565        assert_eq!(decoded, profile);
2566        assert_eq!(
2567            decode_persisted_slot_payload::<Value>(payload.as_slice(), "profile")
2568                .expect("decode raw value payload"),
2569            profile.to_value(),
2570        );
2571    }
2572
2573    #[test]
2574    fn custom_many_slot_payload_roundtrips_structured_value_lists() {
2575        let profiles = vec![
2576            PersistedRowProfileValue {
2577                bio: "Ada".to_string(),
2578            },
2579            PersistedRowProfileValue {
2580                bio: "Grace".to_string(),
2581            },
2582        ];
2583        let payload = encode_persisted_custom_many_slot_payload(profiles.as_slice(), "profiles")
2584            .expect("encode custom structured list payload");
2585        let decoded = decode_persisted_custom_many_slot_payload::<PersistedRowProfileValue>(
2586            payload.as_slice(),
2587            "profiles",
2588        )
2589        .expect("decode custom structured list payload");
2590
2591        assert_eq!(decoded, profiles);
2592    }
2593
2594    #[test]
2595    fn decode_persisted_non_null_slot_payload_rejects_null_for_required_structured_fields() {
2596        let err =
2597            decode_persisted_non_null_slot_payload::<PersistedRowProfileValue>(&[0xF6], "profile")
2598                .expect_err("required structured payload must reject null");
2599
2600        assert!(
2601            err.message
2602                .contains("unexpected null for non-nullable field"),
2603            "unexpected error: {err:?}"
2604        );
2605    }
2606
2607    #[test]
2608    fn decode_persisted_option_slot_payload_treats_cbor_null_as_none() {
2609        let decoded =
2610            decode_persisted_option_slot_payload::<PersistedRowProfileValue>(&[0xF6], "profile")
2611                .expect("optional structured payload should decode");
2612
2613        assert_eq!(decoded, None);
2614    }
2615
2616    #[test]
2617    fn encode_slot_value_from_value_rejects_null_for_required_structured_slots() {
2618        let err = encode_slot_value_from_value(&REQUIRED_STRUCTURED_MODEL, 0, &Value::Null)
2619            .expect_err("required structured slot must reject null");
2620
2621        assert!(
2622            err.message.contains("required field cannot store null"),
2623            "unexpected error: {err:?}"
2624        );
2625    }
2626
2627    #[test]
2628    fn encode_slot_value_from_value_allows_null_for_optional_structured_slots() {
2629        let payload = encode_slot_value_from_value(&OPTIONAL_STRUCTURED_MODEL, 0, &Value::Null)
2630            .expect("optional structured slot should allow null");
2631        let decoded =
2632            decode_slot_value_from_bytes(&OPTIONAL_STRUCTURED_MODEL, 0, payload.as_slice())
2633                .expect("optional structured slot should decode");
2634
2635        assert_eq!(decoded, Value::Null);
2636    }
2637
2638    #[test]
2639    fn decode_slot_value_from_bytes_allows_null_for_optional_account_slots() {
2640        let payload = encode_slot_value_from_value(&OPTIONAL_ACCOUNT_MODEL, 0, &Value::Null)
2641            .expect("optional account slot should allow null");
2642        let decoded = decode_slot_value_from_bytes(&OPTIONAL_ACCOUNT_MODEL, 0, payload.as_slice())
2643            .expect("optional account slot should decode");
2644
2645        assert_eq!(decoded, Value::Null);
2646    }
2647
2648    #[test]
2649    fn structural_slot_reader_accepts_null_for_optional_account_slots() {
2650        let mut writer = SlotBufferWriter::for_model(&OPTIONAL_ACCOUNT_MODEL);
2651        let payload = encode_slot_value_from_value(&OPTIONAL_ACCOUNT_MODEL, 0, &Value::Null)
2652            .expect("optional account slot should allow null");
2653        writer
2654            .write_slot(0, Some(payload.as_slice()))
2655            .expect("write optional account slot");
2656        let raw_row = RawRow::try_new(
2657            serialize_row_payload(writer.finish().expect("finish slot payload"))
2658                .expect("serialize row payload"),
2659        )
2660        .expect("build raw row");
2661
2662        let mut reader = StructuralSlotReader::from_raw_row(&raw_row, &OPTIONAL_ACCOUNT_MODEL)
2663            .expect("row-open validation should accept null optional account slots");
2664
2665        assert_eq!(reader.get_value(0).expect("decode slot"), Some(Value::Null));
2666    }
2667
2668    #[test]
2669    fn encode_slot_value_from_value_rejects_unknown_enum_payload_variants() {
2670        let err = encode_slot_value_from_value(
2671            &ENUM_MODEL,
2672            0,
2673            &Value::Enum(
2674                ValueEnum::new("Unknown", Some("tests::State")).with_payload(Value::Uint(7)),
2675            ),
2676        )
2677        .expect_err("unknown enum payload should fail closed");
2678
2679        assert!(err.message.contains("unknown enum variant"));
2680    }
2681
2682    #[test]
2683    fn structural_slot_reader_and_direct_decode_share_the_same_field_codec_boundary() {
2684        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2685        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2686            .expect("encode value-storage payload");
2687        writer
2688            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2689            .expect("write scalar slot");
2690        writer
2691            .write_slot(1, Some(payload.as_slice()))
2692            .expect("write value-storage slot");
2693        let raw_row = RawRow::try_new(
2694            serialize_row_payload(writer.finish().expect("finish slot payload"))
2695                .expect("serialize row payload"),
2696        )
2697        .expect("build raw row");
2698
2699        let direct_slots =
2700            StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL).expect("decode row");
2701        let mut cached_slots =
2702            StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL).expect("decode row");
2703
2704        let direct_name = decode_slot_value_by_contract(&direct_slots, 0).expect("decode name");
2705        let direct_payload =
2706            decode_slot_value_by_contract(&direct_slots, 1).expect("decode payload");
2707        let cached_name = cached_slots.get_value(0).expect("cached name");
2708        let cached_payload = cached_slots.get_value(1).expect("cached payload");
2709
2710        assert_eq!(direct_name, cached_name);
2711        assert_eq!(direct_payload, cached_payload);
2712    }
2713
2714    #[test]
2715    fn structural_slot_reader_validates_declared_slots_but_defers_non_scalar_materialization() {
2716        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2717        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2718            .expect("encode value-storage payload");
2719        writer
2720            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2721            .expect("write scalar slot");
2722        writer
2723            .write_slot(1, Some(payload.as_slice()))
2724            .expect("write value-storage slot");
2725        let raw_row = RawRow::try_new(
2726            serialize_row_payload(writer.finish().expect("finish slot payload"))
2727                .expect("serialize row payload"),
2728        )
2729        .expect("build raw row");
2730
2731        let mut reader = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
2732            .expect("row-open validation should succeed");
2733
2734        match &reader.cached_values[0] {
2735            CachedSlotValue::Scalar(value) => {
2736                assert_eq!(
2737                    value,
2738                    &Value::Text("Ada".to_string()),
2739                    "scalar slot should stay on the validated scalar fast path",
2740                );
2741            }
2742            other => panic!("expected validated scalar cache for slot 0, found {other:?}"),
2743        }
2744        match &reader.cached_values[1] {
2745            CachedSlotValue::Deferred { materialized } => {
2746                assert!(
2747                    materialized.get().is_none(),
2748                    "non-scalar slot should validate at row-open without eagerly materializing a runtime Value",
2749                );
2750            }
2751            other => panic!("expected deferred cache for slot 1, found {other:?}"),
2752        }
2753
2754        assert_eq!(
2755            reader.get_value(1).expect("decode deferred slot"),
2756            Some(Value::Text("payload".to_string()))
2757        );
2758
2759        match &reader.cached_values[1] {
2760            CachedSlotValue::Deferred { materialized } => {
2761                assert_eq!(
2762                    materialized.get(),
2763                    Some(&Value::Text("payload".to_string())),
2764                    "non-scalar slot should materialize on first semantic access",
2765                );
2766            }
2767            other => panic!("expected deferred cache for slot 1, found {other:?}"),
2768        }
2769    }
2770
2771    #[test]
2772    fn structural_slot_reader_metrics_report_zero_non_scalar_materializations_for_scalar_only_access()
2773     {
2774        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2775        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2776            .expect("encode value-storage payload");
2777        writer
2778            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2779            .expect("write scalar slot");
2780        writer
2781            .write_slot(1, Some(payload.as_slice()))
2782            .expect("write value-storage slot");
2783        let raw_row = RawRow::try_new(
2784            serialize_row_payload(writer.finish().expect("finish slot payload"))
2785                .expect("serialize row payload"),
2786        )
2787        .expect("build raw row");
2788
2789        let (_scalar_read, metrics) = with_structural_read_metrics(|| {
2790            let reader = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
2791                .expect("row-open validation should succeed");
2792
2793            matches!(
2794                reader
2795                    .get_scalar(0)
2796                    .expect("scalar fast path should succeed"),
2797                Some(ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2798            )
2799        });
2800
2801        assert_eq!(metrics.rows_opened, 1);
2802        assert_eq!(metrics.declared_slots_validated, 2);
2803        assert_eq!(metrics.validated_non_scalar_slots, 1);
2804        assert_eq!(
2805            metrics.materialized_non_scalar_slots, 0,
2806            "scalar-only access should not materialize the unused value-storage slot",
2807        );
2808        assert_eq!(metrics.rows_without_lazy_non_scalar_materializations, 1);
2809    }
2810
2811    #[test]
2812    fn structural_slot_reader_metrics_report_one_non_scalar_materialization_on_first_semantic_access()
2813     {
2814        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2815        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2816            .expect("encode value-storage payload");
2817        writer
2818            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2819            .expect("write scalar slot");
2820        writer
2821            .write_slot(1, Some(payload.as_slice()))
2822            .expect("write value-storage slot");
2823        let raw_row = RawRow::try_new(
2824            serialize_row_payload(writer.finish().expect("finish slot payload"))
2825                .expect("serialize row payload"),
2826        )
2827        .expect("build raw row");
2828
2829        let (_value, metrics) = with_structural_read_metrics(|| {
2830            let mut reader = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
2831                .expect("row-open validation should succeed");
2832
2833            reader
2834                .get_value(1)
2835                .expect("deferred slot should materialize")
2836        });
2837
2838        assert_eq!(metrics.rows_opened, 1);
2839        assert_eq!(metrics.declared_slots_validated, 2);
2840        assert_eq!(metrics.validated_non_scalar_slots, 1);
2841        assert_eq!(
2842            metrics.materialized_non_scalar_slots, 1,
2843            "first semantic access should materialize the value-storage slot exactly once",
2844        );
2845        assert_eq!(metrics.rows_without_lazy_non_scalar_materializations, 0);
2846    }
2847
2848    #[test]
2849    fn structural_slot_reader_rejects_malformed_unused_value_storage_slot_at_row_open() {
2850        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2851        writer
2852            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2853            .expect("write scalar slot");
2854        writer
2855            .write_slot(1, Some(&[0xFF]))
2856            .expect("write malformed value-storage slot");
2857        let raw_row = RawRow::try_new(
2858            serialize_row_payload(writer.finish().expect("finish slot payload"))
2859                .expect("serialize row payload"),
2860        )
2861        .expect("build raw row");
2862
2863        let err = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
2864            .err()
2865            .expect("malformed unused value-storage slot must still fail at row-open");
2866
2867        assert!(
2868            err.message.contains("field 'payload'"),
2869            "unexpected error: {err:?}"
2870        );
2871    }
2872
2873    #[test]
2874    fn apply_update_patch_to_raw_row_updates_only_targeted_slots() {
2875        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2876        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2877            .expect("encode value-storage payload");
2878        writer
2879            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2880            .expect("write scalar slot");
2881        writer
2882            .write_slot(1, Some(payload.as_slice()))
2883            .expect("write value-storage slot");
2884        let raw_row = RawRow::try_new(
2885            serialize_row_payload(writer.finish().expect("finish slot payload"))
2886                .expect("serialize row payload"),
2887        )
2888        .expect("build raw row");
2889        let patch = UpdatePatch::new().set(
2890            FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2891            Value::Text("Grace".to_string()),
2892        );
2893
2894        let patched =
2895            apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch).expect("apply patch");
2896        let mut reader =
2897            StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
2898
2899        assert_eq!(
2900            reader.get_value(0).expect("decode slot"),
2901            Some(Value::Text("Grace".to_string()))
2902        );
2903        assert_eq!(
2904            reader.get_value(1).expect("decode slot"),
2905            Some(Value::Text("payload".to_string()))
2906        );
2907    }
2908
2909    #[test]
2910    fn serialize_update_patch_fields_encodes_canonical_slot_payloads() {
2911        let patch = UpdatePatch::new()
2912            .set(
2913                FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2914                Value::Text("Grace".to_string()),
2915            )
2916            .set(
2917                FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2918                Value::Text("payload".to_string()),
2919            );
2920
2921        let serialized =
2922            serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
2923
2924        assert_eq!(serialized.entries().len(), 2);
2925        assert_eq!(
2926            decode_slot_value_from_bytes(
2927                &TEST_MODEL,
2928                serialized.entries()[0].slot().index(),
2929                serialized.entries()[0].payload(),
2930            )
2931            .expect("decode slot payload"),
2932            Value::Text("Grace".to_string())
2933        );
2934        assert_eq!(
2935            decode_slot_value_from_bytes(
2936                &TEST_MODEL,
2937                serialized.entries()[1].slot().index(),
2938                serialized.entries()[1].payload(),
2939            )
2940            .expect("decode slot payload"),
2941            Value::Text("payload".to_string())
2942        );
2943    }
2944
2945    #[test]
2946    fn serialized_patch_writer_rejects_clear_slots() {
2947        let mut writer = SerializedPatchWriter::for_model(&TEST_MODEL);
2948
2949        let err = writer
2950            .write_slot(0, None)
2951            .expect_err("0.65 patch staging must reject missing-slot clears");
2952
2953        assert!(
2954            err.message
2955                .contains("serialized patch writer cannot clear slot 0"),
2956            "unexpected error: {err:?}"
2957        );
2958        assert!(
2959            err.message.contains(TEST_MODEL.path()),
2960            "unexpected error: {err:?}"
2961        );
2962    }
2963
2964    #[test]
2965    fn slot_buffer_writer_rejects_clear_slots() {
2966        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2967
2968        let err = writer
2969            .write_slot(0, None)
2970            .expect_err("canonical row staging must reject missing-slot clears");
2971
2972        assert!(
2973            err.message
2974                .contains("slot buffer writer cannot clear slot 0"),
2975            "unexpected error: {err:?}"
2976        );
2977        assert!(
2978            err.message.contains(TEST_MODEL.path()),
2979            "unexpected error: {err:?}"
2980        );
2981    }
2982
2983    #[test]
2984    fn apply_update_patch_to_raw_row_uses_last_write_wins() {
2985        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2986        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2987            .expect("encode value-storage payload");
2988        writer
2989            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2990            .expect("write scalar slot");
2991        writer
2992            .write_slot(1, Some(payload.as_slice()))
2993            .expect("write value-storage slot");
2994        let raw_row = RawRow::try_new(
2995            serialize_row_payload(writer.finish().expect("finish slot payload"))
2996                .expect("serialize row payload"),
2997        )
2998        .expect("build raw row");
2999        let slot = FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot");
3000        let patch = UpdatePatch::new()
3001            .set(slot, Value::Text("Grace".to_string()))
3002            .set(slot, Value::Text("Lin".to_string()));
3003
3004        let patched =
3005            apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch).expect("apply patch");
3006        let mut reader =
3007            StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
3008
3009        assert_eq!(
3010            reader.get_value(0).expect("decode slot"),
3011            Some(Value::Text("Lin".to_string()))
3012        );
3013    }
3014
3015    #[test]
3016    fn apply_update_patch_to_raw_row_rejects_noncanonical_missing_slot_baseline() {
3017        let empty_slots = vec![None::<&[u8]>; TEST_MODEL.fields().len()];
3018        let raw_row = RawRow::try_new(
3019            serialize_row_payload(
3020                encode_slot_payload_allowing_missing_for_tests(&TEST_MODEL, empty_slots.as_slice())
3021                    .expect("encode malformed slot payload"),
3022            )
3023            .expect("serialize row payload"),
3024        )
3025        .expect("build raw row");
3026        let patch = UpdatePatch::new().set(
3027            FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
3028            Value::Text("payload".to_string()),
3029        );
3030
3031        let err = apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch)
3032            .expect_err("noncanonical rows with missing slots must fail closed");
3033
3034        assert_eq!(err.message, "row decode: missing slot payload: slot=0");
3035    }
3036
3037    #[test]
3038    fn apply_serialized_update_patch_to_raw_row_rejects_noncanonical_scalar_baseline() {
3039        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
3040            .expect("encode value-storage payload");
3041        let malformed_slots = [Some([0xF6].as_slice()), Some(payload.as_slice())];
3042        let raw_row = RawRow::try_new(
3043            serialize_row_payload(
3044                encode_slot_payload_allowing_missing_for_tests(&TEST_MODEL, &malformed_slots)
3045                    .expect("encode malformed slot payload"),
3046            )
3047            .expect("serialize row payload"),
3048        )
3049        .expect("build raw row");
3050        let patch = UpdatePatch::new().set(
3051            FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
3052            Value::Text("patched".to_string()),
3053        );
3054        let serialized =
3055            serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
3056
3057        let err = apply_serialized_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &serialized)
3058            .expect_err("noncanonical scalar baseline must fail closed");
3059
3060        assert!(
3061            err.message.contains("field 'name'"),
3062            "unexpected error: {err:?}"
3063        );
3064        assert!(
3065            err.message
3066                .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
3067            "unexpected error: {err:?}"
3068        );
3069    }
3070
3071    #[test]
3072    fn apply_serialized_update_patch_to_raw_row_rejects_noncanonical_scalar_patch_payload() {
3073        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
3074        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
3075            .expect("encode value-storage payload");
3076        writer
3077            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
3078            .expect("write scalar slot");
3079        writer
3080            .write_slot(1, Some(payload.as_slice()))
3081            .expect("write value-storage slot");
3082        let raw_row = RawRow::try_new(
3083            serialize_row_payload(writer.finish().expect("finish slot payload"))
3084                .expect("serialize row payload"),
3085        )
3086        .expect("build raw row");
3087        let serialized = SerializedUpdatePatch::new(vec![SerializedFieldUpdate::new(
3088            FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
3089            vec![0xF6],
3090        )]);
3091
3092        let err = apply_serialized_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &serialized)
3093            .expect_err("noncanonical serialized patch payload must fail closed");
3094
3095        assert!(
3096            err.message.contains("field 'name'"),
3097            "unexpected error: {err:?}"
3098        );
3099        assert!(
3100            err.message
3101                .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
3102            "unexpected error: {err:?}"
3103        );
3104    }
3105
3106    #[test]
3107    fn structural_slot_reader_rejects_slot_count_mismatch() {
3108        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
3109        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
3110            .expect("encode payload");
3111        writer
3112            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
3113            .expect("write scalar slot");
3114        writer
3115            .write_slot(1, Some(payload.as_slice()))
3116            .expect("write payload slot");
3117        let mut payload = writer.finish().expect("finish slot payload");
3118        payload[..2].copy_from_slice(&1_u16.to_be_bytes());
3119        let raw_row =
3120            RawRow::try_new(serialize_row_payload(payload).expect("serialize row payload"))
3121                .expect("build raw row");
3122
3123        let err = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
3124            .err()
3125            .expect("slot-count drift must fail closed");
3126
3127        assert_eq!(
3128            err.message,
3129            "row decode: slot count mismatch: expected 2, found 1"
3130        );
3131    }
3132
3133    #[test]
3134    fn structural_slot_reader_rejects_slot_span_exceeds_payload_length() {
3135        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
3136        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
3137            .expect("encode payload");
3138        writer
3139            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
3140            .expect("write scalar slot");
3141        writer
3142            .write_slot(1, Some(payload.as_slice()))
3143            .expect("write payload slot");
3144        let mut payload = writer.finish().expect("finish slot payload");
3145
3146        // Corrupt the second slot span so the payload table points past the
3147        // available data section.
3148        payload[14..18].copy_from_slice(&u32::MAX.to_be_bytes());
3149        let raw_row =
3150            RawRow::try_new(serialize_row_payload(payload).expect("serialize row payload"))
3151                .expect("build raw row");
3152
3153        let err = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
3154            .err()
3155            .expect("slot span drift must fail closed");
3156
3157        assert_eq!(err.message, "row decode: slot span exceeds payload length");
3158    }
3159
3160    #[test]
3161    fn apply_serialized_update_patch_to_raw_row_replays_preencoded_slots() {
3162        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
3163        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
3164            .expect("encode value-storage payload");
3165        writer
3166            .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
3167            .expect("write scalar slot");
3168        writer
3169            .write_slot(1, Some(payload.as_slice()))
3170            .expect("write value-storage slot");
3171        let raw_row = RawRow::try_new(
3172            serialize_row_payload(writer.finish().expect("finish slot payload"))
3173                .expect("serialize row payload"),
3174        )
3175        .expect("build raw row");
3176        let patch = UpdatePatch::new().set(
3177            FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
3178            Value::Text("Grace".to_string()),
3179        );
3180        let serialized =
3181            serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
3182
3183        let patched = raw_row
3184            .apply_serialized_update_patch(&TEST_MODEL, &serialized)
3185            .expect("apply serialized patch");
3186        let mut reader =
3187            StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
3188
3189        assert_eq!(
3190            reader.get_value(0).expect("decode slot"),
3191            Some(Value::Text("Grace".to_string()))
3192        );
3193    }
3194
3195    #[test]
3196    fn serialize_entity_slots_as_update_patch_replays_full_typed_after_image() {
3197        let old_entity = PersistedRowPatchBridgeEntity {
3198            id: crate::types::Ulid::from_u128(7),
3199            name: "Ada".to_string(),
3200        };
3201        let new_entity = PersistedRowPatchBridgeEntity {
3202            id: crate::types::Ulid::from_u128(7),
3203            name: "Grace".to_string(),
3204        };
3205        let raw_row = RawRow::from_entity(&old_entity).expect("encode old row");
3206        let old_decoded = raw_row
3207            .try_decode::<PersistedRowPatchBridgeEntity>()
3208            .expect("decode old entity");
3209        let serialized =
3210            serialize_entity_slots_as_update_patch(&new_entity).expect("serialize entity patch");
3211        let direct =
3212            RawRow::from_serialized_update_patch(PersistedRowPatchBridgeEntity::MODEL, &serialized)
3213                .expect("direct row emission should succeed");
3214
3215        let patched = raw_row
3216            .apply_serialized_update_patch(PersistedRowPatchBridgeEntity::MODEL, &serialized)
3217            .expect("apply serialized patch");
3218        let decoded = patched
3219            .try_decode::<PersistedRowPatchBridgeEntity>()
3220            .expect("decode patched entity");
3221
3222        assert_eq!(
3223            direct, patched,
3224            "fresh row emission and replayed full-image patch must converge on identical bytes",
3225        );
3226        assert_eq!(old_decoded, old_entity);
3227        assert_eq!(decoded, new_entity);
3228    }
3229
3230    #[test]
3231    fn canonical_row_from_raw_row_replays_canonical_full_image_bytes() {
3232        let entity = PersistedRowPatchBridgeEntity {
3233            id: crate::types::Ulid::from_u128(11),
3234            name: "Ada".to_string(),
3235        };
3236        let raw_row = RawRow::from_entity(&entity).expect("encode canonical row");
3237        let canonical =
3238            super::canonical_row_from_raw_row(PersistedRowPatchBridgeEntity::MODEL, &raw_row)
3239                .expect("canonical re-emission should succeed");
3240
3241        assert_eq!(
3242            canonical.as_bytes(),
3243            raw_row.as_bytes(),
3244            "canonical raw-row rebuild must preserve already canonical row bytes",
3245        );
3246    }
3247
3248    #[test]
3249    fn canonical_row_from_raw_row_rejects_noncanonical_scalar_payload() {
3250        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
3251            .expect("encode value-storage payload");
3252        let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
3253        writer
3254            .write_slot(0, Some(&[0xF6]))
3255            .expect("write malformed scalar slot");
3256        writer
3257            .write_slot(1, Some(payload.as_slice()))
3258            .expect("write value-storage slot");
3259        let raw_row = RawRow::try_new(
3260            serialize_row_payload(writer.finish().expect("finish slot payload"))
3261                .expect("serialize malformed row"),
3262        )
3263        .expect("build malformed raw row");
3264
3265        let err = super::canonical_row_from_raw_row(&TEST_MODEL, &raw_row)
3266            .expect_err("canonical raw-row rebuild must reject malformed scalar payloads");
3267
3268        assert!(
3269            err.message.contains("field 'name'"),
3270            "unexpected error: {err:?}"
3271        );
3272        assert!(
3273            err.message
3274                .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
3275            "unexpected error: {err:?}"
3276        );
3277    }
3278
3279    #[test]
3280    fn raw_row_from_serialized_update_patch_rejects_noncanonical_scalar_payload() {
3281        let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
3282            .expect("encode value-storage payload");
3283        let serialized = SerializedUpdatePatch::new(vec![
3284            SerializedFieldUpdate::new(
3285                FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
3286                vec![0xF6],
3287            ),
3288            SerializedFieldUpdate::new(
3289                FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
3290                payload,
3291            ),
3292        ]);
3293
3294        let err = RawRow::from_serialized_update_patch(&TEST_MODEL, &serialized)
3295            .expect_err("fresh row emission must reject noncanonical serialized patch payloads");
3296
3297        assert!(
3298            err.message.contains("field 'name'"),
3299            "unexpected error: {err:?}"
3300        );
3301        assert!(
3302            err.message
3303                .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
3304            "unexpected error: {err:?}"
3305        );
3306    }
3307
3308    #[test]
3309    fn raw_row_from_serialized_update_patch_rejects_incomplete_slot_image() {
3310        let serialized = SerializedUpdatePatch::new(vec![SerializedFieldUpdate::new(
3311            FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
3312            crate::serialize::serialize(&Value::Text("payload".to_string()))
3313                .expect("encode value-storage payload"),
3314        )]);
3315
3316        let err = RawRow::from_serialized_update_patch(&TEST_MODEL, &serialized)
3317            .expect_err("fresh row emission must reject missing declared slots");
3318
3319        assert!(
3320            err.message.contains("serialized patch did not emit slot 0"),
3321            "unexpected error: {err:?}"
3322        );
3323    }
3324}