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