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