1mod 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::{borrow::Cow, 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#[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 #[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 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 #[must_use]
81 pub(in crate::db) const fn index(self) -> usize {
82 self.index
83 }
84}
85
86#[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 #[must_use]
108 pub(in crate::db) const fn new(slot: FieldSlot, value: Value) -> Self {
109 Self { slot, value }
110 }
111
112 #[must_use]
114 pub(in crate::db) const fn slot(&self) -> FieldSlot {
115 self.slot
116 }
117
118 #[must_use]
120 pub(in crate::db) const fn value(&self) -> &Value {
121 &self.value
122 }
123}
124
125#[derive(Clone, Debug, Default, Eq, PartialEq)]
137pub struct UpdatePatch {
138 entries: Vec<FieldUpdate>,
139}
140
141impl UpdatePatch {
142 #[must_use]
144 pub const fn new() -> Self {
145 Self {
146 entries: Vec::new(),
147 }
148 }
149
150 #[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 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 #[must_use]
176 pub(in crate::db) const fn entries(&self) -> &[FieldUpdate] {
177 self.entries.as_slice()
178 }
179
180 #[must_use]
182 pub(in crate::db) const fn is_empty(&self) -> bool {
183 self.entries.is_empty()
184 }
185}
186
187#[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 #[must_use]
210 pub(in crate::db) const fn new(slot: FieldSlot, payload: Vec<u8>) -> Self {
211 Self { slot, payload }
212 }
213
214 #[must_use]
216 pub(in crate::db) const fn slot(&self) -> FieldSlot {
217 self.slot
218 }
219
220 #[must_use]
222 pub(in crate::db) const fn payload(&self) -> &[u8] {
223 self.payload.as_slice()
224 }
225}
226
227#[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 #[must_use]
248 pub(in crate::db) const fn new(entries: Vec<SerializedFieldUpdate>) -> Self {
249 Self { entries }
250 }
251
252 #[must_use]
254 pub(in crate::db) const fn entries(&self) -> &[SerializedFieldUpdate] {
255 self.entries.as_slice()
256 }
257
258 #[must_use]
260 pub(in crate::db) const fn is_empty(&self) -> bool {
261 self.entries.is_empty()
262 }
263}
264
265pub trait SlotReader {
274 fn model(&self) -> &'static EntityModel;
276
277 fn has(&self, slot: usize) -> bool;
279
280 fn get_bytes(&self, slot: usize) -> Option<&[u8]>;
282
283 fn get_scalar(&self, slot: usize) -> Result<Option<ScalarSlotValueRef<'_>>, InternalError>;
285
286 fn get_value(&mut self, slot: usize) -> Result<Option<Value>, InternalError>;
288}
289
290pub(in crate::db) trait CanonicalSlotReader: SlotReader {
302 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 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 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 fn required_value_by_contract_cow(&self, slot: usize) -> Result<Cow<'_, Value>, InternalError> {
330 Ok(Cow::Owned(self.required_value_by_contract(slot)?))
331 }
332}
333
334pub trait SlotWriter {
342 fn write_slot(&mut self, slot: usize, payload: Option<&[u8]>) -> Result<(), InternalError>;
344
345 fn write_scalar(
347 &mut self,
348 slot: usize,
349 value: ScalarSlotValueRef<'_>,
350 ) -> Result<(), InternalError> {
351 let payload = encode_scalar_slot_value(value);
352
353 self.write_slot(slot, Some(payload.as_slice()))
354 }
355}
356
357fn slot_cell_mut<T>(slots: &mut [T], slot: usize) -> Result<&mut T, InternalError> {
359 slots.get_mut(slot).ok_or_else(|| {
360 InternalError::persisted_row_encode_failed(
361 format!("slot {slot} is outside the row layout",),
362 )
363 })
364}
365
366fn required_slot_payload_bytes<'a>(
369 model: &'static EntityModel,
370 writer_label: &str,
371 slot: usize,
372 payload: Option<&'a [u8]>,
373) -> Result<&'a [u8], InternalError> {
374 payload.ok_or_else(|| {
375 InternalError::persisted_row_encode_failed(format!(
376 "{writer_label} cannot clear slot {slot} for entity '{}'",
377 model.path()
378 ))
379 })
380}
381
382fn encode_slot_payload_from_parts(
385 slot_count: usize,
386 slot_table: &[(u32, u32)],
387 payload_bytes: &[u8],
388) -> Result<Vec<u8>, InternalError> {
389 let field_count = u16::try_from(slot_count).map_err(|_| {
390 InternalError::persisted_row_encode_failed(format!(
391 "field count {slot_count} exceeds u16 slot table capacity",
392 ))
393 })?;
394 let mut encoded = Vec::with_capacity(
395 usize::from(field_count) * (u32::BITS as usize / 4) + 2 + payload_bytes.len(),
396 );
397 encoded.extend_from_slice(&field_count.to_be_bytes());
398 for (start, len) in slot_table {
399 encoded.extend_from_slice(&start.to_be_bytes());
400 encoded.extend_from_slice(&len.to_be_bytes());
401 }
402 encoded.extend_from_slice(payload_bytes);
403
404 Ok(encoded)
405}
406
407pub trait PersistedRow: EntityKind + Sized {
417 fn materialize_from_slots(slots: &mut dyn SlotReader) -> Result<Self, InternalError>;
419
420 fn write_slots(&self, out: &mut dyn SlotWriter) -> Result<(), InternalError>;
422
423 fn project_slot(slots: &mut dyn SlotReader, slot: usize) -> Result<Option<Value>, InternalError>
425 where
426 Self: crate::traits::FieldProjection,
427 {
428 let entity = Self::materialize_from_slots(slots)?;
429
430 Ok(<Self as crate::traits::FieldProjection>::get_value_by_index(&entity, slot))
431 }
432}
433
434#[cfg(test)]
437pub(in crate::db) fn decode_slot_value_by_contract(
438 slots: &dyn SlotReader,
439 slot: usize,
440) -> Result<Option<Value>, InternalError> {
441 let Some(raw_value) = slots.get_bytes(slot) else {
442 return Ok(None);
443 };
444
445 decode_slot_value_from_bytes(slots.model(), slot, raw_value).map(Some)
446}
447
448pub(in crate::db) fn decode_slot_value_from_bytes(
454 model: &'static EntityModel,
455 slot: usize,
456 raw_value: &[u8],
457) -> Result<Value, InternalError> {
458 let field = field_model_for_slot(model, slot)?;
459
460 decode_slot_value_for_field(field, raw_value)
461}
462
463fn decode_slot_value_for_field(
466 field: &FieldModel,
467 raw_value: &[u8],
468) -> Result<Value, InternalError> {
469 match field.leaf_codec() {
470 LeafCodec::Scalar(codec) => match decode_scalar_slot_value(raw_value, codec, field.name())?
471 {
472 ScalarSlotValueRef::Null => Ok(Value::Null),
473 ScalarSlotValueRef::Value(value) => Ok(value.into_value()),
474 },
475 LeafCodec::CborFallback => decode_non_scalar_slot_value(raw_value, field),
476 }
477}
478
479#[allow(dead_code)]
490pub(in crate::db) fn encode_slot_value_from_value(
491 model: &'static EntityModel,
492 slot: usize,
493 value: &Value,
494) -> Result<Vec<u8>, InternalError> {
495 let field = field_model_for_slot(model, slot)?;
496 ensure_slot_value_matches_field_contract(field, value)?;
497
498 match field.storage_decode() {
499 FieldStorageDecode::Value => serialize(value)
500 .map_err(|err| InternalError::persisted_row_field_encode_failed(field.name(), err)),
501 FieldStorageDecode::ByKind => match field.leaf_codec() {
502 LeafCodec::Scalar(_) => {
503 let scalar = compile_scalar_literal_expr_value(value).ok_or_else(|| {
504 InternalError::persisted_row_field_encode_failed(
505 field.name(),
506 format!(
507 "field kind {:?} requires a scalar runtime value, found {value:?}",
508 field.kind()
509 ),
510 )
511 })?;
512
513 Ok(encode_scalar_slot_value(scalar.as_slot_value_ref()))
514 }
515 LeafCodec::CborFallback => {
516 encode_structural_field_bytes_by_kind(field.kind(), value, field.name())
517 }
518 },
519 }
520}
521
522fn canonicalize_slot_payload(
525 model: &'static EntityModel,
526 slot: usize,
527 raw_value: &[u8],
528) -> Result<Vec<u8>, InternalError> {
529 let value = decode_slot_value_from_bytes(model, slot, raw_value)?;
530
531 encode_slot_value_from_value(model, slot, &value)
532}
533
534fn dense_canonical_slot_image_from_payload_source<'a, F>(
538 model: &'static EntityModel,
539 mut payload_for_slot: F,
540) -> Result<Vec<Vec<u8>>, InternalError>
541where
542 F: FnMut(usize) -> Result<&'a [u8], InternalError>,
543{
544 let mut slot_payloads = Vec::with_capacity(model.fields().len());
545
546 for slot in 0..model.fields().len() {
547 let payload = payload_for_slot(slot)?;
548 slot_payloads.push(canonicalize_slot_payload(model, slot, payload)?);
549 }
550
551 Ok(slot_payloads)
552}
553
554fn dense_canonical_slot_image_from_value_source<'a, F>(
558 model: &'static EntityModel,
559 mut value_for_slot: F,
560) -> Result<Vec<Vec<u8>>, InternalError>
561where
562 F: FnMut(usize) -> Result<Cow<'a, Value>, InternalError>,
563{
564 let mut slot_payloads = Vec::with_capacity(model.fields().len());
565
566 for slot in 0..model.fields().len() {
567 let value = value_for_slot(slot)?;
568 slot_payloads.push(encode_slot_value_from_value(model, slot, value.as_ref())?);
569 }
570
571 Ok(slot_payloads)
572}
573
574fn emit_raw_row_from_slot_payloads(
576 model: &'static EntityModel,
577 slot_payloads: &[Vec<u8>],
578) -> Result<CanonicalRow, InternalError> {
579 if slot_payloads.len() != model.fields().len() {
580 return Err(InternalError::persisted_row_encode_failed(format!(
581 "canonical slot image expected {} slots for entity '{}', found {}",
582 model.fields().len(),
583 model.path(),
584 slot_payloads.len()
585 )));
586 }
587
588 let payload_capacity = slot_payloads
589 .iter()
590 .try_fold(0usize, |len, payload| len.checked_add(payload.len()))
591 .ok_or_else(|| {
592 InternalError::persisted_row_encode_failed(
593 "canonical slot image payload length overflow",
594 )
595 })?;
596 let mut payload_bytes = Vec::with_capacity(payload_capacity);
597 let mut slot_table = Vec::with_capacity(slot_payloads.len());
598
599 for (slot, payload) in slot_payloads.iter().enumerate() {
603 let start = u32::try_from(payload_bytes.len()).map_err(|_| {
604 InternalError::persisted_row_encode_failed(format!(
605 "canonical slot payload start exceeds u32 range: slot={slot}",
606 ))
607 })?;
608 let len = u32::try_from(payload.len()).map_err(|_| {
609 InternalError::persisted_row_encode_failed(format!(
610 "canonical slot payload length exceeds u32 range: slot={slot}",
611 ))
612 })?;
613 payload_bytes.extend_from_slice(payload.as_slice());
614 slot_table.push((start, len));
615 }
616
617 let row_payload =
619 encode_slot_payload_from_parts(slot_payloads.len(), slot_table.as_slice(), &payload_bytes)?;
620 let encoded = serialize_row_payload(row_payload)?;
621 let raw_row = RawRow::from_untrusted_bytes(encoded).map_err(InternalError::from)?;
622
623 Ok(CanonicalRow::from_canonical_raw_row(raw_row))
624}
625
626fn dense_canonical_slot_image_from_serialized_patch(
629 model: &'static EntityModel,
630 patch: &SerializedUpdatePatch,
631) -> Result<Vec<Vec<u8>>, InternalError> {
632 let patch_payloads = serialized_patch_payload_by_slot(model, patch)?;
633
634 dense_canonical_slot_image_from_payload_source(model, |slot| {
635 patch_payloads[slot].ok_or_else(|| {
636 InternalError::persisted_row_encode_failed(format!(
637 "serialized patch did not emit slot {slot} for entity '{}'",
638 model.path()
639 ))
640 })
641 })
642}
643
644pub(in crate::db) fn canonical_row_from_serialized_update_patch(
647 model: &'static EntityModel,
648 patch: &SerializedUpdatePatch,
649) -> Result<CanonicalRow, InternalError> {
650 let slot_payloads = dense_canonical_slot_image_from_serialized_patch(model, patch)?;
651
652 emit_raw_row_from_slot_payloads(model, slot_payloads.as_slice())
653}
654
655pub(in crate::db) fn canonical_row_from_entity<E>(entity: &E) -> Result<CanonicalRow, InternalError>
657where
658 E: PersistedRow,
659{
660 let mut writer = SlotBufferWriter::for_model(E::MODEL);
661
662 entity.write_slots(&mut writer)?;
664
665 let encoded = serialize_row_payload(writer.finish()?)?;
667 let raw_row = RawRow::from_untrusted_bytes(encoded).map_err(InternalError::from)?;
668
669 Ok(CanonicalRow::from_canonical_raw_row(raw_row))
670}
671
672pub(in crate::db) fn canonical_row_from_structural_slot_reader(
674 row_fields: &StructuralSlotReader<'_>,
675) -> Result<CanonicalRow, InternalError> {
676 let slot_payloads = dense_canonical_slot_image_from_value_source(row_fields.model, |slot| {
680 row_fields
681 .required_cached_value(slot)
682 .map(Cow::Borrowed)
683 .map_err(|_| {
684 InternalError::persisted_row_encode_failed(format!(
685 "slot {slot} is missing from the structural value cache for entity '{}'",
686 row_fields.model.path()
687 ))
688 })
689 })?;
690
691 emit_raw_row_from_slot_payloads(row_fields.model, slot_payloads.as_slice())
693}
694
695pub(in crate::db) fn canonical_row_from_raw_row(
698 model: &'static EntityModel,
699 raw_row: &RawRow,
700) -> Result<CanonicalRow, InternalError> {
701 let field_bytes = StructuralRowFieldBytes::from_raw_row(raw_row, model)
702 .map_err(StructuralRowDecodeError::into_internal_error)?;
703
704 let slot_payloads = dense_canonical_slot_image_from_payload_source(model, |slot| {
706 field_bytes.field(slot).ok_or_else(|| {
707 InternalError::persisted_row_encode_failed(format!(
708 "slot {slot} is missing from the baseline row for entity '{}'",
709 model.path()
710 ))
711 })
712 })?;
713
714 emit_raw_row_from_slot_payloads(model, slot_payloads.as_slice())
716}
717
718pub(in crate::db) const fn canonical_row_from_stored_raw_row(raw_row: RawRow) -> CanonicalRow {
720 CanonicalRow::from_canonical_raw_row(raw_row)
721}
722
723#[allow(dead_code)]
726pub(in crate::db) fn apply_update_patch_to_raw_row(
727 model: &'static EntityModel,
728 raw_row: &RawRow,
729 patch: &UpdatePatch,
730) -> Result<CanonicalRow, InternalError> {
731 let serialized_patch = serialize_update_patch_fields(model, patch)?;
732
733 apply_serialized_update_patch_to_raw_row(model, raw_row, &serialized_patch)
734}
735
736#[allow(dead_code)]
742pub(in crate::db) fn serialize_update_patch_fields(
743 model: &'static EntityModel,
744 patch: &UpdatePatch,
745) -> Result<SerializedUpdatePatch, InternalError> {
746 if patch.is_empty() {
747 return Ok(SerializedUpdatePatch::default());
748 }
749
750 let mut entries = Vec::with_capacity(patch.entries().len());
751
752 for entry in patch.entries() {
755 let slot = entry.slot();
756 let payload = encode_slot_value_from_value(model, slot.index(), entry.value())?;
757 entries.push(SerializedFieldUpdate::new(slot, payload));
758 }
759
760 Ok(SerializedUpdatePatch::new(entries))
761}
762
763#[allow(dead_code)]
769pub(in crate::db) fn serialize_entity_slots_as_update_patch<E>(
770 entity: &E,
771) -> Result<SerializedUpdatePatch, InternalError>
772where
773 E: PersistedRow,
774{
775 let mut writer = SerializedPatchWriter::for_model(E::MODEL);
776
777 entity.write_slots(&mut writer)?;
780
781 writer.finish_complete()
784}
785
786#[allow(dead_code)]
791pub(in crate::db) fn apply_serialized_update_patch_to_raw_row(
792 model: &'static EntityModel,
793 raw_row: &RawRow,
794 patch: &SerializedUpdatePatch,
795) -> Result<CanonicalRow, InternalError> {
796 if patch.is_empty() {
797 return canonical_row_from_raw_row(model, raw_row);
798 }
799
800 let field_bytes = StructuralRowFieldBytes::from_raw_row(raw_row, model)
801 .map_err(StructuralRowDecodeError::into_internal_error)?;
802 let patch_payloads = serialized_patch_payload_by_slot(model, patch)?;
803
804 let slot_payloads = dense_canonical_slot_image_from_payload_source(model, |slot| {
808 if let Some(payload) = patch_payloads[slot] {
809 Ok(payload)
810 } else {
811 field_bytes.field(slot).ok_or_else(|| {
812 InternalError::persisted_row_encode_failed(format!(
813 "slot {slot} is missing from the baseline row for entity '{}'",
814 model.path()
815 ))
816 })
817 }
818 })?;
819
820 emit_raw_row_from_slot_payloads(model, slot_payloads.as_slice())
822}
823
824fn decode_non_scalar_slot_value(
827 raw_value: &[u8],
828 field: &FieldModel,
829) -> Result<Value, InternalError> {
830 let decoded = match field.storage_decode() {
831 crate::model::field::FieldStorageDecode::ByKind => {
832 decode_structural_field_by_kind_bytes(raw_value, field.kind())
833 }
834 crate::model::field::FieldStorageDecode::Value => {
835 decode_structural_value_storage_bytes(raw_value)
836 }
837 };
838
839 decoded.map_err(|err| {
840 InternalError::persisted_row_field_kind_decode_failed(field.name(), field.kind(), err)
841 })
842}
843
844#[allow(dead_code)]
847fn ensure_slot_value_matches_field_contract(
848 field: &FieldModel,
849 value: &Value,
850) -> Result<(), InternalError> {
851 if matches!(value, Value::Null) {
852 if field.nullable() {
853 return Ok(());
854 }
855
856 return Err(InternalError::persisted_row_field_encode_failed(
857 field.name(),
858 "required field cannot store null",
859 ));
860 }
861
862 if matches!(field.storage_decode(), FieldStorageDecode::Value) {
866 if !storage_value_matches_field_kind(field.kind(), value) {
867 return Err(InternalError::persisted_row_field_encode_failed(
868 field.name(),
869 format!(
870 "field kind {:?} does not accept runtime value {value:?}",
871 field.kind()
872 ),
873 ));
874 }
875
876 ensure_decimal_scale_matches(field.name(), field.kind(), value)?;
877
878 return ensure_value_is_deterministic_for_storage(field.name(), field.kind(), value);
879 }
880
881 let field_type = field_type_from_model_kind(&field.kind());
882 if !literal_matches_type(value, &field_type) {
883 return Err(InternalError::persisted_row_field_encode_failed(
884 field.name(),
885 format!(
886 "field kind {:?} does not accept runtime value {value:?}",
887 field.kind()
888 ),
889 ));
890 }
891
892 ensure_decimal_scale_matches(field.name(), field.kind(), value)?;
893 ensure_value_is_deterministic_for_storage(field.name(), field.kind(), value)
894}
895
896fn storage_value_matches_field_kind(kind: FieldKind, value: &Value) -> bool {
901 match (kind, value) {
902 (FieldKind::Account, Value::Account(_))
903 | (FieldKind::Blob, Value::Blob(_))
904 | (FieldKind::Bool, Value::Bool(_))
905 | (FieldKind::Date, Value::Date(_))
906 | (FieldKind::Decimal { .. }, Value::Decimal(_))
907 | (FieldKind::Duration, Value::Duration(_))
908 | (FieldKind::Enum { .. }, Value::Enum(_))
909 | (FieldKind::Float32, Value::Float32(_))
910 | (FieldKind::Float64, Value::Float64(_))
911 | (FieldKind::Int, Value::Int(_))
912 | (FieldKind::Int128, Value::Int128(_))
913 | (FieldKind::IntBig, Value::IntBig(_))
914 | (FieldKind::Principal, Value::Principal(_))
915 | (FieldKind::Subaccount, Value::Subaccount(_))
916 | (FieldKind::Text, Value::Text(_))
917 | (FieldKind::Timestamp, Value::Timestamp(_))
918 | (FieldKind::Uint, Value::Uint(_))
919 | (FieldKind::Uint128, Value::Uint128(_))
920 | (FieldKind::UintBig, Value::UintBig(_))
921 | (FieldKind::Ulid, Value::Ulid(_))
922 | (FieldKind::Unit, Value::Unit)
923 | (FieldKind::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
924 (FieldKind::Relation { key_kind, .. }, value) => {
925 storage_value_matches_field_kind(*key_kind, value)
926 }
927 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
928 .iter()
929 .all(|item| storage_value_matches_field_kind(*inner, item)),
930 (FieldKind::Map { key, value }, Value::Map(entries)) => {
931 if Value::validate_map_entries(entries.as_slice()).is_err() {
932 return false;
933 }
934
935 entries.iter().all(|(entry_key, entry_value)| {
936 storage_value_matches_field_kind(*key, entry_key)
937 && storage_value_matches_field_kind(*value, entry_value)
938 })
939 }
940 _ => false,
941 }
942}
943
944#[allow(dead_code)]
947fn ensure_decimal_scale_matches(
948 field_name: &str,
949 kind: FieldKind,
950 value: &Value,
951) -> Result<(), InternalError> {
952 if matches!(value, Value::Null) {
953 return Ok(());
954 }
955
956 match (kind, value) {
957 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
958 if decimal.scale() != scale {
959 return Err(InternalError::persisted_row_field_encode_failed(
960 field_name,
961 format!(
962 "decimal scale mismatch: expected {scale}, found {}",
963 decimal.scale()
964 ),
965 ));
966 }
967
968 Ok(())
969 }
970 (FieldKind::Relation { key_kind, .. }, value) => {
971 ensure_decimal_scale_matches(field_name, *key_kind, value)
972 }
973 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
974 for item in items {
975 ensure_decimal_scale_matches(field_name, *inner, item)?;
976 }
977
978 Ok(())
979 }
980 (
981 FieldKind::Map {
982 key,
983 value: map_value,
984 },
985 Value::Map(entries),
986 ) => {
987 for (entry_key, entry_value) in entries {
988 ensure_decimal_scale_matches(field_name, *key, entry_key)?;
989 ensure_decimal_scale_matches(field_name, *map_value, entry_value)?;
990 }
991
992 Ok(())
993 }
994 _ => Ok(()),
995 }
996}
997
998#[allow(dead_code)]
1001fn ensure_value_is_deterministic_for_storage(
1002 field_name: &str,
1003 kind: FieldKind,
1004 value: &Value,
1005) -> Result<(), InternalError> {
1006 match (kind, value) {
1007 (FieldKind::Set(_), Value::List(items)) => {
1008 for pair in items.windows(2) {
1009 let [left, right] = pair else {
1010 continue;
1011 };
1012 if Value::canonical_cmp(left, right) != Ordering::Less {
1013 return Err(InternalError::persisted_row_field_encode_failed(
1014 field_name,
1015 "set payload must already be canonical and deduplicated",
1016 ));
1017 }
1018 }
1019
1020 Ok(())
1021 }
1022 (FieldKind::Map { .. }, Value::Map(entries)) => {
1023 Value::validate_map_entries(entries.as_slice())
1024 .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err))?;
1025
1026 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1027 return Err(InternalError::persisted_row_field_encode_failed(
1028 field_name,
1029 "map payload must already be canonical and deduplicated",
1030 ));
1031 }
1032
1033 Ok(())
1034 }
1035 _ => Ok(()),
1036 }
1037}
1038
1039fn serialized_patch_payload_by_slot<'a>(
1041 model: &'static EntityModel,
1042 patch: &'a SerializedUpdatePatch,
1043) -> Result<Vec<Option<&'a [u8]>>, InternalError> {
1044 let mut payloads = vec![None; model.fields().len()];
1045
1046 for entry in patch.entries() {
1047 let slot = entry.slot().index();
1048 field_model_for_slot(model, slot)?;
1049 payloads[slot] = Some(entry.payload());
1050 }
1051
1052 Ok(payloads)
1053}
1054
1055fn encode_structural_field_bytes_by_kind(
1058 kind: FieldKind,
1059 value: &Value,
1060 field_name: &str,
1061) -> Result<Vec<u8>, InternalError> {
1062 let cbor_value = encode_structural_field_cbor_by_kind(kind, value, field_name)?;
1063
1064 serialize(&cbor_value)
1065 .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err))
1066}
1067
1068fn encode_structural_field_cbor_by_kind(
1070 kind: FieldKind,
1071 value: &Value,
1072 field_name: &str,
1073) -> Result<CborValue, InternalError> {
1074 match (kind, value) {
1075 (_, Value::Null) => Ok(CborValue::Null),
1076 (FieldKind::Blob, Value::Blob(value)) => Ok(CborValue::Bytes(value.clone())),
1077 (FieldKind::Bool, Value::Bool(value)) => Ok(CborValue::Bool(*value)),
1078 (FieldKind::Text, Value::Text(value)) => Ok(CborValue::Text(value.clone())),
1079 (FieldKind::Int, Value::Int(value)) => Ok(CborValue::Integer(i128::from(*value))),
1080 (FieldKind::Uint, Value::Uint(value)) => Ok(CborValue::Integer(i128::from(*value))),
1081 (FieldKind::Float32, Value::Float32(value)) => to_cbor_value(value)
1082 .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err)),
1083 (FieldKind::Float64, Value::Float64(value)) => to_cbor_value(value)
1084 .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err)),
1085 (FieldKind::Int128, Value::Int128(value)) => to_cbor_value(value)
1086 .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err)),
1087 (FieldKind::Uint128, Value::Uint128(value)) => to_cbor_value(value)
1088 .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err)),
1089 (FieldKind::Ulid, Value::Ulid(value)) => Ok(CborValue::Text(value.to_string())),
1090 (FieldKind::Account, Value::Account(value)) => encode_leaf_cbor_value(value, field_name),
1091 (FieldKind::Date, Value::Date(value)) => encode_leaf_cbor_value(value, field_name),
1092 (FieldKind::Decimal { .. }, Value::Decimal(value)) => {
1093 encode_leaf_cbor_value(value, field_name)
1094 }
1095 (FieldKind::Duration, Value::Duration(value)) => encode_leaf_cbor_value(value, field_name),
1096 (FieldKind::IntBig, Value::IntBig(value)) => encode_leaf_cbor_value(value, field_name),
1097 (FieldKind::Principal, Value::Principal(value)) => {
1098 encode_leaf_cbor_value(value, field_name)
1099 }
1100 (FieldKind::Subaccount, Value::Subaccount(value)) => {
1101 encode_leaf_cbor_value(value, field_name)
1102 }
1103 (FieldKind::Timestamp, Value::Timestamp(value)) => {
1104 encode_leaf_cbor_value(value, field_name)
1105 }
1106 (FieldKind::UintBig, Value::UintBig(value)) => encode_leaf_cbor_value(value, field_name),
1107 (FieldKind::Unit, Value::Unit) => encode_leaf_cbor_value(&(), field_name),
1108 (FieldKind::Relation { key_kind, .. }, value) => {
1109 encode_structural_field_cbor_by_kind(*key_kind, value, field_name)
1110 }
1111 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1112 Ok(CborValue::Array(
1113 items
1114 .iter()
1115 .map(|item| encode_structural_field_cbor_by_kind(*inner, item, field_name))
1116 .collect::<Result<Vec<_>, _>>()?,
1117 ))
1118 }
1119 (FieldKind::Map { key, value }, Value::Map(entries)) => {
1120 let mut encoded = BTreeMap::new();
1121 for (entry_key, entry_value) in entries {
1122 encoded.insert(
1123 encode_structural_field_cbor_by_kind(*key, entry_key, field_name)?,
1124 encode_structural_field_cbor_by_kind(*value, entry_value, field_name)?,
1125 );
1126 }
1127
1128 Ok(CborValue::Map(encoded))
1129 }
1130 (FieldKind::Enum { path, variants }, Value::Enum(value)) => {
1131 encode_enum_cbor_value(path, variants, value, field_name)
1132 }
1133 (FieldKind::Structured { .. }, _) => Err(InternalError::persisted_row_field_encode_failed(
1134 field_name,
1135 "structured ByKind field encoding is unsupported",
1136 )),
1137 _ => Err(InternalError::persisted_row_field_encode_failed(
1138 field_name,
1139 format!("field kind {kind:?} does not accept runtime value {value:?}"),
1140 )),
1141 }
1142}
1143
1144fn encode_leaf_cbor_value<T>(value: &T, field_name: &str) -> Result<CborValue, InternalError>
1146where
1147 T: serde::Serialize,
1148{
1149 to_cbor_value(value)
1150 .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err))
1151}
1152
1153fn encode_enum_cbor_value(
1156 path: &'static str,
1157 variants: &'static [crate::model::field::EnumVariantModel],
1158 value: &ValueEnum,
1159 field_name: &str,
1160) -> Result<CborValue, InternalError> {
1161 if let Some(actual_path) = value.path()
1162 && actual_path != path
1163 {
1164 return Err(InternalError::persisted_row_field_encode_failed(
1165 field_name,
1166 format!("enum path mismatch: expected '{path}', found '{actual_path}'"),
1167 ));
1168 }
1169
1170 let Some(payload) = value.payload() else {
1171 return Ok(CborValue::Text(value.variant().to_string()));
1172 };
1173
1174 let Some(variant_model) = variants.iter().find(|item| item.ident() == value.variant()) else {
1175 return Err(InternalError::persisted_row_field_encode_failed(
1176 field_name,
1177 format!(
1178 "unknown enum variant '{}' for path '{path}'",
1179 value.variant()
1180 ),
1181 ));
1182 };
1183 let Some(payload_kind) = variant_model.payload_kind() else {
1184 return Err(InternalError::persisted_row_field_encode_failed(
1185 field_name,
1186 format!(
1187 "enum variant '{}' does not accept a payload",
1188 value.variant()
1189 ),
1190 ));
1191 };
1192
1193 let payload_value = match variant_model.payload_storage_decode() {
1194 FieldStorageDecode::ByKind => {
1195 encode_structural_field_cbor_by_kind(*payload_kind, payload, field_name)?
1196 }
1197 FieldStorageDecode::Value => to_cbor_value(payload)
1198 .map_err(|err| InternalError::persisted_row_field_encode_failed(field_name, err))?,
1199 };
1200
1201 let mut encoded = BTreeMap::new();
1202 encoded.insert(CborValue::Text(value.variant().to_string()), payload_value);
1203
1204 Ok(CborValue::Map(encoded))
1205}
1206
1207fn field_model_for_slot(
1209 model: &'static EntityModel,
1210 slot: usize,
1211) -> Result<&'static FieldModel, InternalError> {
1212 model
1213 .fields()
1214 .get(slot)
1215 .ok_or_else(|| InternalError::persisted_row_slot_lookup_out_of_bounds(model.path(), slot))
1216}
1217
1218pub(in crate::db) struct SlotBufferWriter {
1226 model: &'static EntityModel,
1227 slots: Vec<SlotBufferSlot>,
1228}
1229
1230impl SlotBufferWriter {
1231 pub(in crate::db) fn for_model(model: &'static EntityModel) -> Self {
1233 Self {
1234 model,
1235 slots: vec![SlotBufferSlot::Missing; model.fields().len()],
1236 }
1237 }
1238
1239 pub(in crate::db) fn finish(self) -> Result<Vec<u8>, InternalError> {
1241 let slot_count = self.slots.len();
1242 let mut payload_bytes = Vec::new();
1243 let mut slot_table = Vec::with_capacity(slot_count);
1244
1245 for (slot, slot_payload) in self.slots.into_iter().enumerate() {
1248 match slot_payload {
1249 SlotBufferSlot::Set(bytes) => {
1250 let start = u32::try_from(payload_bytes.len()).map_err(|_| {
1251 InternalError::persisted_row_encode_failed(
1252 "slot payload start exceeds u32 range",
1253 )
1254 })?;
1255 let len = u32::try_from(bytes.len()).map_err(|_| {
1256 InternalError::persisted_row_encode_failed(
1257 "slot payload length exceeds u32 range",
1258 )
1259 })?;
1260 payload_bytes.extend_from_slice(&bytes);
1261 slot_table.push((start, len));
1262 }
1263 SlotBufferSlot::Missing => {
1264 return Err(InternalError::persisted_row_encode_failed(format!(
1265 "slot buffer writer did not emit slot {slot} for entity '{}'",
1266 self.model.path()
1267 )));
1268 }
1269 }
1270 }
1271
1272 encode_slot_payload_from_parts(slot_count, slot_table.as_slice(), payload_bytes.as_slice())
1274 }
1275}
1276
1277impl SlotWriter for SlotBufferWriter {
1278 fn write_slot(&mut self, slot: usize, payload: Option<&[u8]>) -> Result<(), InternalError> {
1279 let entry = slot_cell_mut(self.slots.as_mut_slice(), slot)?;
1280 let payload = required_slot_payload_bytes(self.model, "slot buffer writer", slot, payload)?;
1281 *entry = SlotBufferSlot::Set(payload.to_vec());
1282
1283 Ok(())
1284 }
1285}
1286
1287#[derive(Clone, Debug, Eq, PartialEq)]
1295enum SlotBufferSlot {
1296 Missing,
1297 Set(Vec<u8>),
1298}
1299
1300struct SerializedPatchWriter {
1313 model: &'static EntityModel,
1314 slots: Vec<PatchWriterSlot>,
1315}
1316
1317impl SerializedPatchWriter {
1318 fn for_model(model: &'static EntityModel) -> Self {
1320 Self {
1321 model,
1322 slots: vec![PatchWriterSlot::Missing; model.fields().len()],
1323 }
1324 }
1325
1326 fn finish_complete(self) -> Result<SerializedUpdatePatch, InternalError> {
1329 let mut entries = Vec::with_capacity(self.slots.len());
1330
1331 for (slot, payload) in self.slots.into_iter().enumerate() {
1334 let field_slot = FieldSlot::from_index(self.model, slot)?;
1335 let serialized = match payload {
1336 PatchWriterSlot::Set(payload) => SerializedFieldUpdate::new(field_slot, payload),
1337 PatchWriterSlot::Missing => {
1338 return Err(InternalError::persisted_row_encode_failed(format!(
1339 "serialized patch writer did not emit slot {slot} for entity '{}'",
1340 self.model.path()
1341 )));
1342 }
1343 };
1344 entries.push(serialized);
1345 }
1346
1347 Ok(SerializedUpdatePatch::new(entries))
1348 }
1349}
1350
1351impl SlotWriter for SerializedPatchWriter {
1352 fn write_slot(&mut self, slot: usize, payload: Option<&[u8]>) -> Result<(), InternalError> {
1353 let entry = slot_cell_mut(self.slots.as_mut_slice(), slot)?;
1354 let payload =
1355 required_slot_payload_bytes(self.model, "serialized patch writer", slot, payload)?;
1356 *entry = PatchWriterSlot::Set(payload.to_vec());
1357
1358 Ok(())
1359 }
1360}
1361
1362#[derive(Clone, Debug, Eq, PartialEq)]
1374enum PatchWriterSlot {
1375 Missing,
1376 Set(Vec<u8>),
1377}
1378
1379pub(in crate::db) struct StructuralSlotReader<'a> {
1390 model: &'static EntityModel,
1391 field_bytes: StructuralRowFieldBytes<'a>,
1392 cached_values: Vec<CachedSlotValue>,
1393}
1394
1395impl<'a> StructuralSlotReader<'a> {
1396 pub(in crate::db) fn from_raw_row(
1398 raw_row: &'a RawRow,
1399 model: &'static EntityModel,
1400 ) -> Result<Self, InternalError> {
1401 let field_bytes = StructuralRowFieldBytes::from_raw_row(raw_row, model)
1402 .map_err(StructuralRowDecodeError::into_internal_error)?;
1403 let cached_values = std::iter::repeat_with(|| CachedSlotValue::Pending)
1404 .take(model.fields().len())
1405 .collect();
1406 let mut reader = Self {
1407 model,
1408 field_bytes,
1409 cached_values,
1410 };
1411
1412 reader.decode_all_declared_slots()?;
1416
1417 Ok(reader)
1418 }
1419
1420 pub(in crate::db) fn validate_storage_key(
1422 &self,
1423 data_key: &DataKey,
1424 ) -> Result<(), InternalError> {
1425 self.validate_storage_key_value(data_key.storage_key())
1426 }
1427
1428 pub(in crate::db) fn validate_storage_key_value(
1431 &self,
1432 expected_key: StorageKey,
1433 ) -> Result<(), InternalError> {
1434 let Some(primary_key_slot) = resolve_primary_key_slot(self.model) else {
1435 return Err(InternalError::persisted_row_primary_key_field_missing(
1436 self.model.path(),
1437 ));
1438 };
1439 let field = self.field_model(primary_key_slot)?;
1440 let decoded_key = match self.get_scalar(primary_key_slot)? {
1441 Some(ScalarSlotValueRef::Null) => None,
1442 Some(ScalarSlotValueRef::Value(value)) => storage_key_from_scalar_ref(value),
1443 None => Some(
1444 decode_storage_key_field_bytes(
1445 self.required_field_bytes(primary_key_slot, field.name())?,
1446 field.kind,
1447 )
1448 .map_err(|err| {
1449 InternalError::persisted_row_primary_key_not_storage_encodable(
1450 expected_key,
1451 err,
1452 )
1453 })?,
1454 ),
1455 };
1456 let Some(decoded_key) = decoded_key else {
1457 return Err(InternalError::persisted_row_primary_key_slot_missing(
1458 expected_key,
1459 ));
1460 };
1461
1462 if decoded_key != expected_key {
1463 return Err(InternalError::persisted_row_key_mismatch(
1464 expected_key,
1465 decoded_key,
1466 ));
1467 }
1468
1469 Ok(())
1470 }
1471
1472 fn field_model(&self, slot: usize) -> Result<&FieldModel, InternalError> {
1474 field_model_for_slot(self.model, slot)
1475 }
1476
1477 fn decode_all_declared_slots(&mut self) -> Result<(), InternalError> {
1480 for slot in 0..self.model.fields().len() {
1481 self.decode_slot_into_cache(slot)?;
1482 }
1483
1484 Ok(())
1485 }
1486
1487 fn decode_slot_into_cache(&mut self, slot: usize) -> Result<(), InternalError> {
1491 if matches!(
1492 self.cached_values.get(slot),
1493 Some(CachedSlotValue::Decoded(_))
1494 ) {
1495 return Ok(());
1496 }
1497
1498 let field = self.field_model(slot)?;
1499 let value =
1500 decode_slot_value_for_field(field, self.required_field_bytes(slot, field.name())?)?;
1501 self.cached_values[slot] = CachedSlotValue::Decoded(value);
1502
1503 Ok(())
1504 }
1505
1506 pub(in crate::db) fn into_decoded_values(self) -> Result<Vec<Option<Value>>, InternalError> {
1511 let mut values = Vec::with_capacity(self.cached_values.len());
1512
1513 for (slot, cached) in self.cached_values.into_iter().enumerate() {
1514 match cached {
1515 CachedSlotValue::Decoded(value) => values.push(Some(value)),
1516 CachedSlotValue::Pending => {
1517 return Err(InternalError::persisted_row_decode_failed(format!(
1518 "structural slot cache was not fully decoded before consumption: slot={slot}",
1519 )));
1520 }
1521 }
1522 }
1523
1524 Ok(values)
1525 }
1526
1527 fn required_cached_value(&self, slot: usize) -> Result<&Value, InternalError> {
1531 let cached = self.cached_values.get(slot).ok_or_else(|| {
1532 InternalError::persisted_row_slot_cache_lookup_out_of_bounds(self.model.path(), slot)
1533 })?;
1534
1535 match cached {
1536 CachedSlotValue::Decoded(value) => Ok(value),
1537 CachedSlotValue::Pending => Err(InternalError::persisted_row_decode_failed(format!(
1538 "structural slot cache missing decoded value after eager decode: slot={slot}",
1539 ))),
1540 }
1541 }
1542
1543 pub(in crate::db) fn required_field_bytes(
1546 &self,
1547 slot: usize,
1548 field_name: &str,
1549 ) -> Result<&[u8], InternalError> {
1550 self.field_bytes
1551 .field(slot)
1552 .ok_or_else(|| InternalError::persisted_row_declared_field_missing(field_name))
1553 }
1554}
1555
1556const fn storage_key_from_scalar_ref(value: ScalarValueRef<'_>) -> Option<StorageKey> {
1559 match value {
1560 ScalarValueRef::Int(value) => Some(StorageKey::Int(value)),
1561 ScalarValueRef::Principal(value) => Some(StorageKey::Principal(value)),
1562 ScalarValueRef::Subaccount(value) => Some(StorageKey::Subaccount(value)),
1563 ScalarValueRef::Timestamp(value) => Some(StorageKey::Timestamp(value)),
1564 ScalarValueRef::Uint(value) => Some(StorageKey::Uint(value)),
1565 ScalarValueRef::Ulid(value) => Some(StorageKey::Ulid(value)),
1566 ScalarValueRef::Unit => Some(StorageKey::Unit),
1567 _ => None,
1568 }
1569}
1570
1571fn scalar_slot_value_ref_from_cached_value(
1573 value: &Value,
1574) -> Result<ScalarSlotValueRef<'_>, InternalError> {
1575 let scalar = match value {
1576 Value::Null => return Ok(ScalarSlotValueRef::Null),
1577 Value::Blob(value) => ScalarValueRef::Blob(value.as_slice()),
1578 Value::Bool(value) => ScalarValueRef::Bool(*value),
1579 Value::Date(value) => ScalarValueRef::Date(*value),
1580 Value::Duration(value) => ScalarValueRef::Duration(*value),
1581 Value::Float32(value) => ScalarValueRef::Float32(*value),
1582 Value::Float64(value) => ScalarValueRef::Float64(*value),
1583 Value::Int(value) => ScalarValueRef::Int(*value),
1584 Value::Principal(value) => ScalarValueRef::Principal(*value),
1585 Value::Subaccount(value) => ScalarValueRef::Subaccount(*value),
1586 Value::Text(value) => ScalarValueRef::Text(value.as_str()),
1587 Value::Timestamp(value) => ScalarValueRef::Timestamp(*value),
1588 Value::Uint(value) => ScalarValueRef::Uint(*value),
1589 Value::Ulid(value) => ScalarValueRef::Ulid(*value),
1590 Value::Unit => ScalarValueRef::Unit,
1591 _ => {
1592 return Err(InternalError::persisted_row_decode_failed(format!(
1593 "cached structural scalar slot cannot borrow non-scalar value variant: {value:?}",
1594 )));
1595 }
1596 };
1597
1598 Ok(ScalarSlotValueRef::Value(scalar))
1599}
1600
1601impl SlotReader for StructuralSlotReader<'_> {
1602 fn model(&self) -> &'static EntityModel {
1603 self.model
1604 }
1605
1606 fn has(&self, slot: usize) -> bool {
1607 self.field_bytes.field(slot).is_some()
1608 }
1609
1610 fn get_bytes(&self, slot: usize) -> Option<&[u8]> {
1611 self.field_bytes.field(slot)
1612 }
1613
1614 fn get_scalar(&self, slot: usize) -> Result<Option<ScalarSlotValueRef<'_>>, InternalError> {
1615 let field = self.field_model(slot)?;
1616
1617 match field.leaf_codec() {
1618 LeafCodec::Scalar(_codec) => {
1619 scalar_slot_value_ref_from_cached_value(self.required_cached_value(slot)?).map(Some)
1620 }
1621 LeafCodec::CborFallback => Ok(None),
1622 }
1623 }
1624
1625 fn get_value(&mut self, slot: usize) -> Result<Option<Value>, InternalError> {
1626 self.decode_slot_into_cache(slot)?;
1627 Ok(Some(self.required_cached_value(slot)?.clone()))
1628 }
1629}
1630
1631impl CanonicalSlotReader for StructuralSlotReader<'_> {
1632 fn required_value_by_contract(&self, slot: usize) -> Result<Value, InternalError> {
1633 Ok(self.required_cached_value(slot)?.clone())
1634 }
1635
1636 fn required_value_by_contract_cow(&self, slot: usize) -> Result<Cow<'_, Value>, InternalError> {
1637 Ok(Cow::Borrowed(self.required_cached_value(slot)?))
1638 }
1639}
1640
1641enum CachedSlotValue {
1649 Pending,
1650 Decoded(Value),
1651}
1652
1653#[cfg(test)]
1658mod tests {
1659 use super::{
1660 FieldSlot, ScalarSlotValueRef, ScalarValueRef, SerializedFieldUpdate,
1661 SerializedPatchWriter, SerializedUpdatePatch, SlotBufferWriter, SlotReader, SlotWriter,
1662 UpdatePatch, apply_serialized_update_patch_to_raw_row, apply_update_patch_to_raw_row,
1663 decode_persisted_custom_many_slot_payload, decode_persisted_custom_slot_payload,
1664 decode_persisted_non_null_slot_payload, decode_persisted_option_slot_payload,
1665 decode_persisted_slot_payload, decode_slot_value_by_contract, decode_slot_value_from_bytes,
1666 encode_persisted_custom_many_slot_payload, encode_persisted_custom_slot_payload,
1667 encode_scalar_slot_value, encode_slot_payload_from_parts, encode_slot_value_from_value,
1668 serialize_entity_slots_as_update_patch, serialize_update_patch_fields,
1669 };
1670 use crate::{
1671 db::{
1672 codec::serialize_row_payload,
1673 data::{RawRow, StructuralSlotReader, decode_structural_value_storage_bytes},
1674 },
1675 error::InternalError,
1676 model::{
1677 EntityModel,
1678 field::{EnumVariantModel, FieldKind, FieldModel, FieldStorageDecode},
1679 },
1680 serialize::serialize,
1681 testing::SIMPLE_ENTITY_TAG,
1682 traits::{EntitySchema, FieldValue},
1683 types::{
1684 Account, Date, Decimal, Duration, Float32, Float64, Int, Int128, Nat, Nat128,
1685 Principal, Subaccount, Timestamp, Ulid,
1686 },
1687 value::{Value, ValueEnum},
1688 };
1689 use icydb_derive::{FieldProjection, PersistedRow};
1690 use serde::{Deserialize, Serialize};
1691
1692 crate::test_canister! {
1693 ident = PersistedRowPatchBridgeCanister,
1694 commit_memory_id = crate::testing::test_commit_memory_id(),
1695 }
1696
1697 crate::test_store! {
1698 ident = PersistedRowPatchBridgeStore,
1699 canister = PersistedRowPatchBridgeCanister,
1700 }
1701
1702 #[derive(
1714 Clone, Debug, Default, Deserialize, FieldProjection, PartialEq, PersistedRow, Serialize,
1715 )]
1716 struct PersistedRowPatchBridgeEntity {
1717 id: crate::types::Ulid,
1718 name: String,
1719 }
1720
1721 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1722 struct PersistedRowProfileValue {
1723 bio: String,
1724 }
1725
1726 impl FieldValue for PersistedRowProfileValue {
1727 fn kind() -> crate::traits::FieldValueKind {
1728 crate::traits::FieldValueKind::Structured { queryable: false }
1729 }
1730
1731 fn to_value(&self) -> Value {
1732 Value::from_map(vec![(
1733 Value::Text("bio".to_string()),
1734 Value::Text(self.bio.clone()),
1735 )])
1736 .expect("profile test value should encode as canonical map")
1737 }
1738
1739 fn from_value(value: &Value) -> Option<Self> {
1740 let Value::Map(entries) = value else {
1741 return None;
1742 };
1743 let normalized = Value::normalize_map_entries(entries.clone()).ok()?;
1744 let bio = normalized
1745 .iter()
1746 .find_map(|(entry_key, entry_value)| match entry_key {
1747 Value::Text(entry_key) if entry_key == "bio" => match entry_value {
1748 Value::Text(bio) => Some(bio.clone()),
1749 _ => None,
1750 },
1751 _ => None,
1752 })?;
1753
1754 if normalized.len() != 1 {
1755 return None;
1756 }
1757
1758 Some(Self { bio })
1759 }
1760 }
1761
1762 crate::test_entity_schema! {
1763 ident = PersistedRowPatchBridgeEntity,
1764 id = crate::types::Ulid,
1765 id_field = id,
1766 entity_name = "PersistedRowPatchBridgeEntity",
1767 entity_tag = SIMPLE_ENTITY_TAG,
1768 pk_index = 0,
1769 fields = [
1770 ("id", FieldKind::Ulid),
1771 ("name", FieldKind::Text),
1772 ],
1773 indexes = [],
1774 store = PersistedRowPatchBridgeStore,
1775 canister = PersistedRowPatchBridgeCanister,
1776 }
1777
1778 static STATE_VARIANTS: &[EnumVariantModel] = &[EnumVariantModel::new(
1779 "Loaded",
1780 Some(&FieldKind::Uint),
1781 FieldStorageDecode::ByKind,
1782 )];
1783 static FIELD_MODELS: [FieldModel; 2] = [
1784 FieldModel::new("name", FieldKind::Text),
1785 FieldModel::new_with_storage_decode("payload", FieldKind::Text, FieldStorageDecode::Value),
1786 ];
1787 static LIST_FIELD_MODELS: [FieldModel; 1] =
1788 [FieldModel::new("tags", FieldKind::List(&FieldKind::Text))];
1789 static MAP_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
1790 "props",
1791 FieldKind::Map {
1792 key: &FieldKind::Text,
1793 value: &FieldKind::Uint,
1794 },
1795 )];
1796 static ENUM_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
1797 "state",
1798 FieldKind::Enum {
1799 path: "tests::State",
1800 variants: STATE_VARIANTS,
1801 },
1802 )];
1803 static ACCOUNT_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new("owner", FieldKind::Account)];
1804 static REQUIRED_STRUCTURED_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
1805 "profile",
1806 FieldKind::Structured { queryable: false },
1807 )];
1808 static OPTIONAL_STRUCTURED_FIELD_MODELS: [FieldModel; 1] =
1809 [FieldModel::new_with_storage_decode_and_nullability(
1810 "profile",
1811 FieldKind::Structured { queryable: false },
1812 FieldStorageDecode::ByKind,
1813 true,
1814 )];
1815 static VALUE_STORAGE_STRUCTURED_FIELD_MODELS: [FieldModel; 1] =
1816 [FieldModel::new_with_storage_decode(
1817 "manifest",
1818 FieldKind::Structured { queryable: false },
1819 FieldStorageDecode::Value,
1820 )];
1821 static STRUCTURED_MAP_VALUE_KIND: FieldKind = FieldKind::Structured { queryable: false };
1822 static STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS: [FieldModel; 1] =
1823 [FieldModel::new_with_storage_decode(
1824 "projects",
1825 FieldKind::Map {
1826 key: &FieldKind::Principal,
1827 value: &STRUCTURED_MAP_VALUE_KIND,
1828 },
1829 FieldStorageDecode::Value,
1830 )];
1831 static INDEX_MODELS: [&crate::model::index::IndexModel; 0] = [];
1832 static TEST_MODEL: EntityModel = EntityModel::new(
1833 "tests::PersistedRowFieldCodecEntity",
1834 "persisted_row_field_codec_entity",
1835 &FIELD_MODELS[0],
1836 &FIELD_MODELS,
1837 &INDEX_MODELS,
1838 );
1839 static LIST_MODEL: EntityModel = EntityModel::new(
1840 "tests::PersistedRowListFieldCodecEntity",
1841 "persisted_row_list_field_codec_entity",
1842 &LIST_FIELD_MODELS[0],
1843 &LIST_FIELD_MODELS,
1844 &INDEX_MODELS,
1845 );
1846 static MAP_MODEL: EntityModel = EntityModel::new(
1847 "tests::PersistedRowMapFieldCodecEntity",
1848 "persisted_row_map_field_codec_entity",
1849 &MAP_FIELD_MODELS[0],
1850 &MAP_FIELD_MODELS,
1851 &INDEX_MODELS,
1852 );
1853 static ENUM_MODEL: EntityModel = EntityModel::new(
1854 "tests::PersistedRowEnumFieldCodecEntity",
1855 "persisted_row_enum_field_codec_entity",
1856 &ENUM_FIELD_MODELS[0],
1857 &ENUM_FIELD_MODELS,
1858 &INDEX_MODELS,
1859 );
1860 static ACCOUNT_MODEL: EntityModel = EntityModel::new(
1861 "tests::PersistedRowAccountFieldCodecEntity",
1862 "persisted_row_account_field_codec_entity",
1863 &ACCOUNT_FIELD_MODELS[0],
1864 &ACCOUNT_FIELD_MODELS,
1865 &INDEX_MODELS,
1866 );
1867 static REQUIRED_STRUCTURED_MODEL: EntityModel = EntityModel::new(
1868 "tests::PersistedRowRequiredStructuredFieldCodecEntity",
1869 "persisted_row_required_structured_field_codec_entity",
1870 &REQUIRED_STRUCTURED_FIELD_MODELS[0],
1871 &REQUIRED_STRUCTURED_FIELD_MODELS,
1872 &INDEX_MODELS,
1873 );
1874 static OPTIONAL_STRUCTURED_MODEL: EntityModel = EntityModel::new(
1875 "tests::PersistedRowOptionalStructuredFieldCodecEntity",
1876 "persisted_row_optional_structured_field_codec_entity",
1877 &OPTIONAL_STRUCTURED_FIELD_MODELS[0],
1878 &OPTIONAL_STRUCTURED_FIELD_MODELS,
1879 &INDEX_MODELS,
1880 );
1881 static VALUE_STORAGE_STRUCTURED_MODEL: EntityModel = EntityModel::new(
1882 "tests::PersistedRowValueStorageStructuredFieldCodecEntity",
1883 "persisted_row_value_storage_structured_field_codec_entity",
1884 &VALUE_STORAGE_STRUCTURED_FIELD_MODELS[0],
1885 &VALUE_STORAGE_STRUCTURED_FIELD_MODELS,
1886 &INDEX_MODELS,
1887 );
1888 static STRUCTURED_MAP_VALUE_STORAGE_MODEL: EntityModel = EntityModel::new(
1889 "tests::PersistedRowStructuredMapValueStorageEntity",
1890 "persisted_row_structured_map_value_storage_entity",
1891 &STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS[0],
1892 &STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS,
1893 &INDEX_MODELS,
1894 );
1895
1896 fn representative_value_storage_cases() -> Vec<Value> {
1897 let nested = Value::from_map(vec![
1898 (
1899 Value::Text("blob".to_string()),
1900 Value::Blob(vec![0x10, 0x20, 0x30]),
1901 ),
1902 (
1903 Value::Text("i128".to_string()),
1904 Value::Int128(Int128::from(-123i128)),
1905 ),
1906 (
1907 Value::Text("u128".to_string()),
1908 Value::Uint128(Nat128::from(456u128)),
1909 ),
1910 (
1911 Value::Text("enum".to_string()),
1912 Value::Enum(
1913 ValueEnum::new("Loaded", Some("tests::PersistedRowManifest"))
1914 .with_payload(Value::Blob(vec![0xAA, 0xBB])),
1915 ),
1916 ),
1917 ])
1918 .expect("nested value storage case should normalize");
1919
1920 vec![
1921 Value::Account(Account::dummy(7)),
1922 Value::Blob(vec![1u8, 2u8, 3u8]),
1923 Value::Bool(true),
1924 Value::Date(Date::new(2024, 1, 2)),
1925 Value::Decimal(Decimal::new(123, 2)),
1926 Value::Duration(Duration::from_secs(1)),
1927 Value::Enum(
1928 ValueEnum::new("Ready", Some("tests::PersistedRowState"))
1929 .with_payload(nested.clone()),
1930 ),
1931 Value::Float32(Float32::try_new(1.25).expect("float32 sample should be finite")),
1932 Value::Float64(Float64::try_new(2.5).expect("float64 sample should be finite")),
1933 Value::Int(-7),
1934 Value::Int128(Int128::from(123i128)),
1935 Value::IntBig(Int::from(99i32)),
1936 Value::List(vec![
1937 Value::Blob(vec![0xCC, 0xDD]),
1938 Value::Text("nested".to_string()),
1939 nested.clone(),
1940 ]),
1941 nested,
1942 Value::Null,
1943 Value::Principal(Principal::dummy(9)),
1944 Value::Subaccount(Subaccount::new([7u8; 32])),
1945 Value::Text("example".to_string()),
1946 Value::Timestamp(Timestamp::from_secs(1)),
1947 Value::Uint(7),
1948 Value::Uint128(Nat128::from(9u128)),
1949 Value::UintBig(Nat::from(11u64)),
1950 Value::Ulid(Ulid::from_u128(42)),
1951 Value::Unit,
1952 ]
1953 }
1954
1955 fn representative_structured_value_storage_cases() -> Vec<Value> {
1956 let nested_map = Value::from_map(vec![
1957 (
1958 Value::Text("account".to_string()),
1959 Value::Account(Account::dummy(7)),
1960 ),
1961 (
1962 Value::Text("blob".to_string()),
1963 Value::Blob(vec![1u8, 2u8, 3u8]),
1964 ),
1965 (Value::Text("bool".to_string()), Value::Bool(true)),
1966 (
1967 Value::Text("date".to_string()),
1968 Value::Date(Date::new(2024, 1, 2)),
1969 ),
1970 (
1971 Value::Text("decimal".to_string()),
1972 Value::Decimal(Decimal::new(123, 2)),
1973 ),
1974 (
1975 Value::Text("duration".to_string()),
1976 Value::Duration(Duration::from_secs(1)),
1977 ),
1978 (
1979 Value::Text("enum".to_string()),
1980 Value::Enum(
1981 ValueEnum::new("Loaded", Some("tests::PersistedRowManifest"))
1982 .with_payload(Value::Blob(vec![0xAA, 0xBB])),
1983 ),
1984 ),
1985 (
1986 Value::Text("f32".to_string()),
1987 Value::Float32(Float32::try_new(1.25).expect("float32 sample should be finite")),
1988 ),
1989 (
1990 Value::Text("f64".to_string()),
1991 Value::Float64(Float64::try_new(2.5).expect("float64 sample should be finite")),
1992 ),
1993 (Value::Text("i64".to_string()), Value::Int(-7)),
1994 (
1995 Value::Text("i128".to_string()),
1996 Value::Int128(Int128::from(123i128)),
1997 ),
1998 (
1999 Value::Text("ibig".to_string()),
2000 Value::IntBig(Int::from(99i32)),
2001 ),
2002 (Value::Text("null".to_string()), Value::Null),
2003 (
2004 Value::Text("principal".to_string()),
2005 Value::Principal(Principal::dummy(9)),
2006 ),
2007 (
2008 Value::Text("subaccount".to_string()),
2009 Value::Subaccount(Subaccount::new([7u8; 32])),
2010 ),
2011 (
2012 Value::Text("text".to_string()),
2013 Value::Text("example".to_string()),
2014 ),
2015 (
2016 Value::Text("timestamp".to_string()),
2017 Value::Timestamp(Timestamp::from_secs(1)),
2018 ),
2019 (Value::Text("u64".to_string()), Value::Uint(7)),
2020 (
2021 Value::Text("u128".to_string()),
2022 Value::Uint128(Nat128::from(9u128)),
2023 ),
2024 (
2025 Value::Text("ubig".to_string()),
2026 Value::UintBig(Nat::from(11u64)),
2027 ),
2028 (
2029 Value::Text("ulid".to_string()),
2030 Value::Ulid(Ulid::from_u128(42)),
2031 ),
2032 (Value::Text("unit".to_string()), Value::Unit),
2033 ])
2034 .expect("structured value-storage map should normalize");
2035
2036 vec![
2037 nested_map.clone(),
2038 Value::List(vec![
2039 Value::Blob(vec![0xCC, 0xDD]),
2040 Value::Text("nested".to_string()),
2041 nested_map,
2042 ]),
2043 ]
2044 }
2045
2046 fn encode_slot_payload_allowing_missing_for_tests(
2047 model: &'static EntityModel,
2048 slots: &[Option<&[u8]>],
2049 ) -> Result<Vec<u8>, InternalError> {
2050 if slots.len() != model.fields().len() {
2051 return Err(InternalError::persisted_row_encode_failed(format!(
2052 "noncanonical slot payload test helper expected {} slots for entity '{}', found {}",
2053 model.fields().len(),
2054 model.path(),
2055 slots.len()
2056 )));
2057 }
2058 let mut payload_bytes = Vec::new();
2059 let mut slot_table = Vec::with_capacity(slots.len());
2060
2061 for slot_payload in slots {
2062 match slot_payload {
2063 Some(bytes) => {
2064 let start = u32::try_from(payload_bytes.len()).map_err(|_| {
2065 InternalError::persisted_row_encode_failed(
2066 "slot payload start exceeds u32 range",
2067 )
2068 })?;
2069 let len = u32::try_from(bytes.len()).map_err(|_| {
2070 InternalError::persisted_row_encode_failed(
2071 "slot payload length exceeds u32 range",
2072 )
2073 })?;
2074 payload_bytes.extend_from_slice(bytes);
2075 slot_table.push((start, len));
2076 }
2077 None => slot_table.push((0_u32, 0_u32)),
2078 }
2079 }
2080
2081 encode_slot_payload_from_parts(slots.len(), slot_table.as_slice(), payload_bytes.as_slice())
2082 }
2083
2084 #[test]
2085 fn decode_slot_value_from_bytes_decodes_scalar_slots_through_one_owner() {
2086 let payload =
2087 encode_scalar_slot_value(ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")));
2088 let value =
2089 decode_slot_value_from_bytes(&TEST_MODEL, 0, payload.as_slice()).expect("decode slot");
2090
2091 assert_eq!(value, Value::Text("Ada".to_string()));
2092 }
2093
2094 #[test]
2095 fn decode_slot_value_from_bytes_reports_scalar_prefix_bytes() {
2096 let err = decode_slot_value_from_bytes(&TEST_MODEL, 0, &[0x00, 1])
2097 .expect_err("invalid scalar slot prefix should fail closed");
2098
2099 assert!(
2100 err.message
2101 .contains("expected slot envelope prefix byte 0xFF, found 0x00"),
2102 "unexpected error: {err:?}"
2103 );
2104 }
2105
2106 #[test]
2107 fn decode_slot_value_from_bytes_respects_value_storage_decode_contract() {
2108 let payload = crate::serialize::serialize(&Value::Text("Ada".to_string()))
2109 .expect("encode value-storage payload");
2110
2111 let value =
2112 decode_slot_value_from_bytes(&TEST_MODEL, 1, payload.as_slice()).expect("decode slot");
2113
2114 assert_eq!(value, Value::Text("Ada".to_string()));
2115 }
2116
2117 #[test]
2118 fn encode_slot_value_from_value_roundtrips_scalar_slots() {
2119 let payload = encode_slot_value_from_value(&TEST_MODEL, 0, &Value::Text("Ada".to_string()))
2120 .expect("encode slot");
2121 let decoded =
2122 decode_slot_value_from_bytes(&TEST_MODEL, 0, payload.as_slice()).expect("decode slot");
2123
2124 assert_eq!(decoded, Value::Text("Ada".to_string()));
2125 }
2126
2127 #[test]
2128 fn encode_slot_value_from_value_roundtrips_value_storage_slots() {
2129 let payload = encode_slot_value_from_value(&TEST_MODEL, 1, &Value::Text("Ada".to_string()))
2130 .expect("encode slot");
2131 let decoded =
2132 decode_slot_value_from_bytes(&TEST_MODEL, 1, payload.as_slice()).expect("decode slot");
2133
2134 assert_eq!(decoded, Value::Text("Ada".to_string()));
2135 }
2136
2137 #[test]
2138 fn encode_slot_value_from_value_roundtrips_structured_value_storage_slots_for_all_cases() {
2139 for value in representative_structured_value_storage_cases() {
2140 let payload = encode_slot_value_from_value(&VALUE_STORAGE_STRUCTURED_MODEL, 0, &value)
2141 .unwrap_or_else(|err| {
2142 panic!(
2143 "structured value-storage slot should encode for value {value:?}: {err:?}"
2144 )
2145 });
2146 let decoded = decode_slot_value_from_bytes(
2147 &VALUE_STORAGE_STRUCTURED_MODEL,
2148 0,
2149 payload.as_slice(),
2150 )
2151 .unwrap_or_else(|err| {
2152 panic!(
2153 "structured value-storage slot should decode for value {value:?} with payload {payload:?}: {err:?}"
2154 )
2155 });
2156
2157 assert_eq!(decoded, value);
2158 }
2159 }
2160
2161 #[test]
2162 fn encode_slot_value_from_value_roundtrips_list_by_kind_slots() {
2163 let payload = encode_slot_value_from_value(
2164 &LIST_MODEL,
2165 0,
2166 &Value::List(vec![Value::Text("alpha".to_string())]),
2167 )
2168 .expect("encode list slot");
2169 let decoded =
2170 decode_slot_value_from_bytes(&LIST_MODEL, 0, payload.as_slice()).expect("decode slot");
2171
2172 assert_eq!(decoded, Value::List(vec![Value::Text("alpha".to_string())]),);
2173 }
2174
2175 #[test]
2176 fn encode_slot_value_from_value_roundtrips_map_by_kind_slots() {
2177 let payload = encode_slot_value_from_value(
2178 &MAP_MODEL,
2179 0,
2180 &Value::Map(vec![(Value::Text("alpha".to_string()), Value::Uint(7))]),
2181 )
2182 .expect("encode map slot");
2183 let decoded =
2184 decode_slot_value_from_bytes(&MAP_MODEL, 0, payload.as_slice()).expect("decode slot");
2185
2186 assert_eq!(
2187 decoded,
2188 Value::Map(vec![(Value::Text("alpha".to_string()), Value::Uint(7))]),
2189 );
2190 }
2191
2192 #[test]
2193 fn encode_slot_value_from_value_accepts_value_storage_maps_with_structured_values() {
2194 let principal = Principal::dummy(7);
2195 let project = Value::from_map(vec![
2196 (Value::Text("pid".to_string()), Value::Principal(principal)),
2197 (
2198 Value::Text("status".to_string()),
2199 Value::Enum(ValueEnum::new(
2200 "Saved",
2201 Some("design::app::user::customise::project::ProjectStatus"),
2202 )),
2203 ),
2204 ])
2205 .expect("project value should normalize into a canonical map");
2206 let projects = Value::from_map(vec![(Value::Principal(principal), project)])
2207 .expect("outer map should normalize into a canonical map");
2208
2209 let payload =
2210 encode_slot_value_from_value(&STRUCTURED_MAP_VALUE_STORAGE_MODEL, 0, &projects)
2211 .expect("encode structured map slot");
2212 let decoded = decode_slot_value_from_bytes(
2213 &STRUCTURED_MAP_VALUE_STORAGE_MODEL,
2214 0,
2215 payload.as_slice(),
2216 )
2217 .expect("decode structured map slot");
2218
2219 assert_eq!(decoded, projects);
2220 }
2221
2222 #[test]
2223 fn structured_value_storage_cases_decode_through_direct_value_storage_boundary() {
2224 for value in representative_value_storage_cases() {
2225 let payload = serialize(&value).unwrap_or_else(|err| {
2226 panic!(
2227 "structured value-storage payload should serialize for value {value:?}: {err:?}"
2228 )
2229 });
2230 let decoded = decode_structural_value_storage_bytes(payload.as_slice()).unwrap_or_else(
2231 |err| {
2232 panic!(
2233 "structured value-storage payload should decode for value {value:?} with payload {payload:?}: {err:?}"
2234 )
2235 },
2236 );
2237
2238 assert_eq!(decoded, value);
2239 }
2240 }
2241
2242 #[test]
2243 fn encode_slot_value_from_value_roundtrips_enum_by_kind_slots() {
2244 let payload = encode_slot_value_from_value(
2245 &ENUM_MODEL,
2246 0,
2247 &Value::Enum(
2248 ValueEnum::new("Loaded", Some("tests::State")).with_payload(Value::Uint(7)),
2249 ),
2250 )
2251 .expect("encode enum slot");
2252 let decoded =
2253 decode_slot_value_from_bytes(&ENUM_MODEL, 0, payload.as_slice()).expect("decode slot");
2254
2255 assert_eq!(
2256 decoded,
2257 Value::Enum(
2258 ValueEnum::new("Loaded", Some("tests::State")).with_payload(Value::Uint(7,))
2259 ),
2260 );
2261 }
2262
2263 #[test]
2264 fn encode_slot_value_from_value_roundtrips_leaf_by_kind_wrapper_slots() {
2265 let account = Account::from_parts(Principal::dummy(7), Some(Subaccount::from([7_u8; 32])));
2266 let payload = encode_slot_value_from_value(&ACCOUNT_MODEL, 0, &Value::Account(account))
2267 .expect("encode account slot");
2268 let decoded = decode_slot_value_from_bytes(&ACCOUNT_MODEL, 0, payload.as_slice())
2269 .expect("decode slot");
2270
2271 assert_eq!(decoded, Value::Account(account));
2272 }
2273
2274 #[test]
2275 fn custom_slot_payload_roundtrips_structured_field_value() {
2276 let profile = PersistedRowProfileValue {
2277 bio: "Ada".to_string(),
2278 };
2279 let payload = encode_persisted_custom_slot_payload(&profile, "profile")
2280 .expect("encode custom structured payload");
2281 let decoded = decode_persisted_custom_slot_payload::<PersistedRowProfileValue>(
2282 payload.as_slice(),
2283 "profile",
2284 )
2285 .expect("decode custom structured payload");
2286
2287 assert_eq!(decoded, profile);
2288 assert_eq!(
2289 decode_persisted_slot_payload::<Value>(payload.as_slice(), "profile")
2290 .expect("decode raw value payload"),
2291 profile.to_value(),
2292 );
2293 }
2294
2295 #[test]
2296 fn custom_many_slot_payload_roundtrips_structured_value_lists() {
2297 let profiles = vec![
2298 PersistedRowProfileValue {
2299 bio: "Ada".to_string(),
2300 },
2301 PersistedRowProfileValue {
2302 bio: "Grace".to_string(),
2303 },
2304 ];
2305 let payload = encode_persisted_custom_many_slot_payload(profiles.as_slice(), "profiles")
2306 .expect("encode custom structured list payload");
2307 let decoded = decode_persisted_custom_many_slot_payload::<PersistedRowProfileValue>(
2308 payload.as_slice(),
2309 "profiles",
2310 )
2311 .expect("decode custom structured list payload");
2312
2313 assert_eq!(decoded, profiles);
2314 }
2315
2316 #[test]
2317 fn decode_persisted_non_null_slot_payload_rejects_null_for_required_structured_fields() {
2318 let err =
2319 decode_persisted_non_null_slot_payload::<PersistedRowProfileValue>(&[0xF6], "profile")
2320 .expect_err("required structured payload must reject null");
2321
2322 assert!(
2323 err.message
2324 .contains("unexpected null for non-nullable field"),
2325 "unexpected error: {err:?}"
2326 );
2327 }
2328
2329 #[test]
2330 fn decode_persisted_option_slot_payload_treats_cbor_null_as_none() {
2331 let decoded =
2332 decode_persisted_option_slot_payload::<PersistedRowProfileValue>(&[0xF6], "profile")
2333 .expect("optional structured payload should decode");
2334
2335 assert_eq!(decoded, None);
2336 }
2337
2338 #[test]
2339 fn encode_slot_value_from_value_rejects_null_for_required_structured_slots() {
2340 let err = encode_slot_value_from_value(&REQUIRED_STRUCTURED_MODEL, 0, &Value::Null)
2341 .expect_err("required structured slot must reject null");
2342
2343 assert!(
2344 err.message.contains("required field cannot store null"),
2345 "unexpected error: {err:?}"
2346 );
2347 }
2348
2349 #[test]
2350 fn encode_slot_value_from_value_allows_null_for_optional_structured_slots() {
2351 let payload = encode_slot_value_from_value(&OPTIONAL_STRUCTURED_MODEL, 0, &Value::Null)
2352 .expect("optional structured slot should allow null");
2353 let decoded =
2354 decode_slot_value_from_bytes(&OPTIONAL_STRUCTURED_MODEL, 0, payload.as_slice())
2355 .expect("optional structured slot should decode");
2356
2357 assert_eq!(decoded, Value::Null);
2358 }
2359
2360 #[test]
2361 fn encode_slot_value_from_value_rejects_unknown_enum_payload_variants() {
2362 let err = encode_slot_value_from_value(
2363 &ENUM_MODEL,
2364 0,
2365 &Value::Enum(
2366 ValueEnum::new("Unknown", Some("tests::State")).with_payload(Value::Uint(7)),
2367 ),
2368 )
2369 .expect_err("unknown enum payload should fail closed");
2370
2371 assert!(err.message.contains("unknown enum variant"));
2372 }
2373
2374 #[test]
2375 fn structural_slot_reader_and_direct_decode_share_the_same_field_codec_boundary() {
2376 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2377 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2378 .expect("encode value-storage payload");
2379 writer
2380 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2381 .expect("write scalar slot");
2382 writer
2383 .write_slot(1, Some(payload.as_slice()))
2384 .expect("write value-storage slot");
2385 let raw_row = RawRow::try_new(
2386 serialize_row_payload(writer.finish().expect("finish slot payload"))
2387 .expect("serialize row payload"),
2388 )
2389 .expect("build raw row");
2390
2391 let direct_slots =
2392 StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL).expect("decode row");
2393 let mut cached_slots =
2394 StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL).expect("decode row");
2395
2396 let direct_name = decode_slot_value_by_contract(&direct_slots, 0).expect("decode name");
2397 let direct_payload =
2398 decode_slot_value_by_contract(&direct_slots, 1).expect("decode payload");
2399 let cached_name = cached_slots.get_value(0).expect("cached name");
2400 let cached_payload = cached_slots.get_value(1).expect("cached payload");
2401
2402 assert_eq!(direct_name, cached_name);
2403 assert_eq!(direct_payload, cached_payload);
2404 }
2405
2406 #[test]
2407 fn apply_update_patch_to_raw_row_updates_only_targeted_slots() {
2408 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2409 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2410 .expect("encode value-storage payload");
2411 writer
2412 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2413 .expect("write scalar slot");
2414 writer
2415 .write_slot(1, Some(payload.as_slice()))
2416 .expect("write value-storage slot");
2417 let raw_row = RawRow::try_new(
2418 serialize_row_payload(writer.finish().expect("finish slot payload"))
2419 .expect("serialize row payload"),
2420 )
2421 .expect("build raw row");
2422 let patch = UpdatePatch::new().set(
2423 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2424 Value::Text("Grace".to_string()),
2425 );
2426
2427 let patched =
2428 apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch).expect("apply patch");
2429 let mut reader =
2430 StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
2431
2432 assert_eq!(
2433 reader.get_value(0).expect("decode slot"),
2434 Some(Value::Text("Grace".to_string()))
2435 );
2436 assert_eq!(
2437 reader.get_value(1).expect("decode slot"),
2438 Some(Value::Text("payload".to_string()))
2439 );
2440 }
2441
2442 #[test]
2443 fn serialize_update_patch_fields_encodes_canonical_slot_payloads() {
2444 let patch = UpdatePatch::new()
2445 .set(
2446 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2447 Value::Text("Grace".to_string()),
2448 )
2449 .set(
2450 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2451 Value::Text("payload".to_string()),
2452 );
2453
2454 let serialized =
2455 serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
2456
2457 assert_eq!(serialized.entries().len(), 2);
2458 assert_eq!(
2459 decode_slot_value_from_bytes(
2460 &TEST_MODEL,
2461 serialized.entries()[0].slot().index(),
2462 serialized.entries()[0].payload(),
2463 )
2464 .expect("decode slot payload"),
2465 Value::Text("Grace".to_string())
2466 );
2467 assert_eq!(
2468 decode_slot_value_from_bytes(
2469 &TEST_MODEL,
2470 serialized.entries()[1].slot().index(),
2471 serialized.entries()[1].payload(),
2472 )
2473 .expect("decode slot payload"),
2474 Value::Text("payload".to_string())
2475 );
2476 }
2477
2478 #[test]
2479 fn serialized_patch_writer_rejects_clear_slots() {
2480 let mut writer = SerializedPatchWriter::for_model(&TEST_MODEL);
2481
2482 let err = writer
2483 .write_slot(0, None)
2484 .expect_err("0.65 patch staging must reject missing-slot clears");
2485
2486 assert!(
2487 err.message
2488 .contains("serialized patch writer cannot clear slot 0"),
2489 "unexpected error: {err:?}"
2490 );
2491 assert!(
2492 err.message.contains(TEST_MODEL.path()),
2493 "unexpected error: {err:?}"
2494 );
2495 }
2496
2497 #[test]
2498 fn slot_buffer_writer_rejects_clear_slots() {
2499 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2500
2501 let err = writer
2502 .write_slot(0, None)
2503 .expect_err("canonical row staging must reject missing-slot clears");
2504
2505 assert!(
2506 err.message
2507 .contains("slot buffer writer cannot clear slot 0"),
2508 "unexpected error: {err:?}"
2509 );
2510 assert!(
2511 err.message.contains(TEST_MODEL.path()),
2512 "unexpected error: {err:?}"
2513 );
2514 }
2515
2516 #[test]
2517 fn apply_update_patch_to_raw_row_uses_last_write_wins() {
2518 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2519 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2520 .expect("encode value-storage payload");
2521 writer
2522 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2523 .expect("write scalar slot");
2524 writer
2525 .write_slot(1, Some(payload.as_slice()))
2526 .expect("write value-storage slot");
2527 let raw_row = RawRow::try_new(
2528 serialize_row_payload(writer.finish().expect("finish slot payload"))
2529 .expect("serialize row payload"),
2530 )
2531 .expect("build raw row");
2532 let slot = FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot");
2533 let patch = UpdatePatch::new()
2534 .set(slot, Value::Text("Grace".to_string()))
2535 .set(slot, Value::Text("Lin".to_string()));
2536
2537 let patched =
2538 apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch).expect("apply patch");
2539 let mut reader =
2540 StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
2541
2542 assert_eq!(
2543 reader.get_value(0).expect("decode slot"),
2544 Some(Value::Text("Lin".to_string()))
2545 );
2546 }
2547
2548 #[test]
2549 fn apply_update_patch_to_raw_row_rejects_noncanonical_missing_slot_baseline() {
2550 let empty_slots = vec![None::<&[u8]>; TEST_MODEL.fields().len()];
2551 let raw_row = RawRow::try_new(
2552 serialize_row_payload(
2553 encode_slot_payload_allowing_missing_for_tests(&TEST_MODEL, empty_slots.as_slice())
2554 .expect("encode malformed slot payload"),
2555 )
2556 .expect("serialize row payload"),
2557 )
2558 .expect("build raw row");
2559 let patch = UpdatePatch::new().set(
2560 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2561 Value::Text("payload".to_string()),
2562 );
2563
2564 let err = apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch)
2565 .expect_err("noncanonical rows with missing slots must fail closed");
2566
2567 assert_eq!(err.message, "row decode: missing slot payload: slot=0");
2568 }
2569
2570 #[test]
2571 fn apply_serialized_update_patch_to_raw_row_rejects_noncanonical_scalar_baseline() {
2572 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2573 .expect("encode value-storage payload");
2574 let malformed_slots = [Some([0xF6].as_slice()), Some(payload.as_slice())];
2575 let raw_row = RawRow::try_new(
2576 serialize_row_payload(
2577 encode_slot_payload_allowing_missing_for_tests(&TEST_MODEL, &malformed_slots)
2578 .expect("encode malformed slot payload"),
2579 )
2580 .expect("serialize row payload"),
2581 )
2582 .expect("build raw row");
2583 let patch = UpdatePatch::new().set(
2584 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2585 Value::Text("patched".to_string()),
2586 );
2587 let serialized =
2588 serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
2589
2590 let err = apply_serialized_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &serialized)
2591 .expect_err("noncanonical scalar baseline must fail closed");
2592
2593 assert!(
2594 err.message.contains("field 'name'"),
2595 "unexpected error: {err:?}"
2596 );
2597 assert!(
2598 err.message
2599 .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
2600 "unexpected error: {err:?}"
2601 );
2602 }
2603
2604 #[test]
2605 fn apply_serialized_update_patch_to_raw_row_rejects_noncanonical_scalar_patch_payload() {
2606 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2607 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2608 .expect("encode value-storage payload");
2609 writer
2610 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2611 .expect("write scalar slot");
2612 writer
2613 .write_slot(1, Some(payload.as_slice()))
2614 .expect("write value-storage slot");
2615 let raw_row = RawRow::try_new(
2616 serialize_row_payload(writer.finish().expect("finish slot payload"))
2617 .expect("serialize row payload"),
2618 )
2619 .expect("build raw row");
2620 let serialized = SerializedUpdatePatch::new(vec![SerializedFieldUpdate::new(
2621 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2622 vec![0xF6],
2623 )]);
2624
2625 let err = apply_serialized_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &serialized)
2626 .expect_err("noncanonical serialized patch payload must fail closed");
2627
2628 assert!(
2629 err.message.contains("field 'name'"),
2630 "unexpected error: {err:?}"
2631 );
2632 assert!(
2633 err.message
2634 .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
2635 "unexpected error: {err:?}"
2636 );
2637 }
2638
2639 #[test]
2640 fn structural_slot_reader_rejects_slot_count_mismatch() {
2641 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2642 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2643 .expect("encode payload");
2644 writer
2645 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2646 .expect("write scalar slot");
2647 writer
2648 .write_slot(1, Some(payload.as_slice()))
2649 .expect("write payload slot");
2650 let mut payload = writer.finish().expect("finish slot payload");
2651 payload[..2].copy_from_slice(&1_u16.to_be_bytes());
2652 let raw_row =
2653 RawRow::try_new(serialize_row_payload(payload).expect("serialize row payload"))
2654 .expect("build raw row");
2655
2656 let err = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
2657 .err()
2658 .expect("slot-count drift must fail closed");
2659
2660 assert_eq!(
2661 err.message,
2662 "row decode: slot count mismatch: expected 2, found 1"
2663 );
2664 }
2665
2666 #[test]
2667 fn structural_slot_reader_rejects_slot_span_exceeds_payload_length() {
2668 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2669 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2670 .expect("encode payload");
2671 writer
2672 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2673 .expect("write scalar slot");
2674 writer
2675 .write_slot(1, Some(payload.as_slice()))
2676 .expect("write payload slot");
2677 let mut payload = writer.finish().expect("finish slot payload");
2678
2679 payload[14..18].copy_from_slice(&u32::MAX.to_be_bytes());
2682 let raw_row =
2683 RawRow::try_new(serialize_row_payload(payload).expect("serialize row payload"))
2684 .expect("build raw row");
2685
2686 let err = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
2687 .err()
2688 .expect("slot span drift must fail closed");
2689
2690 assert_eq!(err.message, "row decode: slot span exceeds payload length");
2691 }
2692
2693 #[test]
2694 fn apply_serialized_update_patch_to_raw_row_replays_preencoded_slots() {
2695 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2696 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2697 .expect("encode value-storage payload");
2698 writer
2699 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2700 .expect("write scalar slot");
2701 writer
2702 .write_slot(1, Some(payload.as_slice()))
2703 .expect("write value-storage slot");
2704 let raw_row = RawRow::try_new(
2705 serialize_row_payload(writer.finish().expect("finish slot payload"))
2706 .expect("serialize row payload"),
2707 )
2708 .expect("build raw row");
2709 let patch = UpdatePatch::new().set(
2710 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2711 Value::Text("Grace".to_string()),
2712 );
2713 let serialized =
2714 serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
2715
2716 let patched = raw_row
2717 .apply_serialized_update_patch(&TEST_MODEL, &serialized)
2718 .expect("apply serialized patch");
2719 let mut reader =
2720 StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
2721
2722 assert_eq!(
2723 reader.get_value(0).expect("decode slot"),
2724 Some(Value::Text("Grace".to_string()))
2725 );
2726 }
2727
2728 #[test]
2729 fn serialize_entity_slots_as_update_patch_replays_full_typed_after_image() {
2730 let old_entity = PersistedRowPatchBridgeEntity {
2731 id: crate::types::Ulid::from_u128(7),
2732 name: "Ada".to_string(),
2733 };
2734 let new_entity = PersistedRowPatchBridgeEntity {
2735 id: crate::types::Ulid::from_u128(7),
2736 name: "Grace".to_string(),
2737 };
2738 let raw_row = RawRow::from_entity(&old_entity).expect("encode old row");
2739 let old_decoded = raw_row
2740 .try_decode::<PersistedRowPatchBridgeEntity>()
2741 .expect("decode old entity");
2742 let serialized =
2743 serialize_entity_slots_as_update_patch(&new_entity).expect("serialize entity patch");
2744 let direct =
2745 RawRow::from_serialized_update_patch(PersistedRowPatchBridgeEntity::MODEL, &serialized)
2746 .expect("direct row emission should succeed");
2747
2748 let patched = raw_row
2749 .apply_serialized_update_patch(PersistedRowPatchBridgeEntity::MODEL, &serialized)
2750 .expect("apply serialized patch");
2751 let decoded = patched
2752 .try_decode::<PersistedRowPatchBridgeEntity>()
2753 .expect("decode patched entity");
2754
2755 assert_eq!(
2756 direct, patched,
2757 "fresh row emission and replayed full-image patch must converge on identical bytes",
2758 );
2759 assert_eq!(old_decoded, old_entity);
2760 assert_eq!(decoded, new_entity);
2761 }
2762
2763 #[test]
2764 fn canonical_row_from_raw_row_replays_canonical_full_image_bytes() {
2765 let entity = PersistedRowPatchBridgeEntity {
2766 id: crate::types::Ulid::from_u128(11),
2767 name: "Ada".to_string(),
2768 };
2769 let raw_row = RawRow::from_entity(&entity).expect("encode canonical row");
2770 let canonical =
2771 super::canonical_row_from_raw_row(PersistedRowPatchBridgeEntity::MODEL, &raw_row)
2772 .expect("canonical re-emission should succeed");
2773
2774 assert_eq!(
2775 canonical.as_bytes(),
2776 raw_row.as_bytes(),
2777 "canonical raw-row rebuild must preserve already canonical row bytes",
2778 );
2779 }
2780
2781 #[test]
2782 fn canonical_row_from_raw_row_rejects_noncanonical_scalar_payload() {
2783 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2784 .expect("encode value-storage payload");
2785 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2786 writer
2787 .write_slot(0, Some(&[0xF6]))
2788 .expect("write malformed scalar slot");
2789 writer
2790 .write_slot(1, Some(payload.as_slice()))
2791 .expect("write value-storage slot");
2792 let raw_row = RawRow::try_new(
2793 serialize_row_payload(writer.finish().expect("finish slot payload"))
2794 .expect("serialize malformed row"),
2795 )
2796 .expect("build malformed raw row");
2797
2798 let err = super::canonical_row_from_raw_row(&TEST_MODEL, &raw_row)
2799 .expect_err("canonical raw-row rebuild must reject malformed scalar payloads");
2800
2801 assert!(
2802 err.message.contains("field 'name'"),
2803 "unexpected error: {err:?}"
2804 );
2805 assert!(
2806 err.message
2807 .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
2808 "unexpected error: {err:?}"
2809 );
2810 }
2811
2812 #[test]
2813 fn raw_row_from_serialized_update_patch_rejects_noncanonical_scalar_payload() {
2814 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2815 .expect("encode value-storage payload");
2816 let serialized = SerializedUpdatePatch::new(vec![
2817 SerializedFieldUpdate::new(
2818 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2819 vec![0xF6],
2820 ),
2821 SerializedFieldUpdate::new(
2822 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2823 payload,
2824 ),
2825 ]);
2826
2827 let err = RawRow::from_serialized_update_patch(&TEST_MODEL, &serialized)
2828 .expect_err("fresh row emission must reject noncanonical serialized patch payloads");
2829
2830 assert!(
2831 err.message.contains("field 'name'"),
2832 "unexpected error: {err:?}"
2833 );
2834 assert!(
2835 err.message
2836 .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
2837 "unexpected error: {err:?}"
2838 );
2839 }
2840
2841 #[test]
2842 fn raw_row_from_serialized_update_patch_rejects_incomplete_slot_image() {
2843 let serialized = SerializedUpdatePatch::new(vec![SerializedFieldUpdate::new(
2844 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2845 crate::serialize::serialize(&Value::Text("payload".to_string()))
2846 .expect("encode value-storage payload"),
2847 )]);
2848
2849 let err = RawRow::from_serialized_update_patch(&TEST_MODEL, &serialized)
2850 .expect_err("fresh row emission must reject missing declared slots");
2851
2852 assert!(
2853 err.message.contains("serialized patch did not emit slot 0"),
2854 "unexpected error: {err:?}"
2855 );
2856 }
2857}