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