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