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