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