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 let _ = self.get_value(slot)?;
1407 }
1408
1409 Ok(())
1410 }
1411
1412 pub(in crate::db) fn required_field_bytes(
1415 &self,
1416 slot: usize,
1417 field_name: &str,
1418 ) -> Result<&[u8], InternalError> {
1419 self.field_bytes
1420 .field(slot)
1421 .ok_or_else(|| InternalError::persisted_row_declared_field_missing(field_name))
1422 }
1423}
1424
1425const fn storage_key_from_scalar_ref(value: ScalarValueRef<'_>) -> Option<StorageKey> {
1428 match value {
1429 ScalarValueRef::Int(value) => Some(StorageKey::Int(value)),
1430 ScalarValueRef::Principal(value) => Some(StorageKey::Principal(value)),
1431 ScalarValueRef::Subaccount(value) => Some(StorageKey::Subaccount(value)),
1432 ScalarValueRef::Timestamp(value) => Some(StorageKey::Timestamp(value)),
1433 ScalarValueRef::Uint(value) => Some(StorageKey::Uint(value)),
1434 ScalarValueRef::Ulid(value) => Some(StorageKey::Ulid(value)),
1435 ScalarValueRef::Unit => Some(StorageKey::Unit),
1436 _ => None,
1437 }
1438}
1439
1440impl SlotReader for StructuralSlotReader<'_> {
1441 fn model(&self) -> &'static EntityModel {
1442 self.model
1443 }
1444
1445 fn has(&self, slot: usize) -> bool {
1446 self.field_bytes.field(slot).is_some()
1447 }
1448
1449 fn get_bytes(&self, slot: usize) -> Option<&[u8]> {
1450 self.field_bytes.field(slot)
1451 }
1452
1453 fn get_scalar(&self, slot: usize) -> Result<Option<ScalarSlotValueRef<'_>>, InternalError> {
1454 let field = self.field_model(slot)?;
1455
1456 match field.leaf_codec() {
1457 LeafCodec::Scalar(codec) => decode_scalar_slot_value(
1458 self.required_field_bytes(slot, field.name())?,
1459 codec,
1460 field.name(),
1461 )
1462 .map(Some),
1463 LeafCodec::CborFallback => Ok(None),
1464 }
1465 }
1466
1467 fn get_value(&mut self, slot: usize) -> Result<Option<Value>, InternalError> {
1468 let cached = self.cached_values.get(slot).ok_or_else(|| {
1469 InternalError::persisted_row_slot_cache_lookup_out_of_bounds(self.model.path(), slot)
1470 })?;
1471 if let CachedSlotValue::Decoded(value) = cached {
1472 return Ok(Some(value.clone()));
1473 }
1474
1475 let field = self.field_model(slot)?;
1476 let value = decode_slot_value_from_bytes(
1477 self.model,
1478 slot,
1479 self.required_field_bytes(slot, field.name())?,
1480 )?;
1481 self.cached_values[slot] = CachedSlotValue::Decoded(value.clone());
1482
1483 Ok(Some(value))
1484 }
1485}
1486
1487impl CanonicalSlotReader for StructuralSlotReader<'_> {}
1488
1489enum CachedSlotValue {
1497 Pending,
1498 Decoded(Value),
1499}
1500
1501#[cfg(test)]
1506mod tests {
1507 use super::{
1508 FieldSlot, ScalarSlotValueRef, ScalarValueRef, SerializedFieldUpdate,
1509 SerializedPatchWriter, SerializedUpdatePatch, SlotBufferWriter, SlotReader, SlotWriter,
1510 UpdatePatch, apply_serialized_update_patch_to_raw_row, apply_update_patch_to_raw_row,
1511 decode_persisted_custom_many_slot_payload, decode_persisted_custom_slot_payload,
1512 decode_persisted_non_null_slot_payload, decode_persisted_option_slot_payload,
1513 decode_persisted_slot_payload, decode_slot_value_by_contract, decode_slot_value_from_bytes,
1514 encode_persisted_custom_many_slot_payload, encode_persisted_custom_slot_payload,
1515 encode_scalar_slot_value, encode_slot_payload_from_parts, encode_slot_value_from_value,
1516 serialize_entity_slots_as_update_patch, serialize_update_patch_fields,
1517 };
1518 use crate::{
1519 db::{
1520 codec::serialize_row_payload,
1521 data::{RawRow, StructuralSlotReader},
1522 },
1523 error::InternalError,
1524 model::{
1525 EntityModel,
1526 field::{EnumVariantModel, FieldKind, FieldModel, FieldStorageDecode},
1527 },
1528 testing::SIMPLE_ENTITY_TAG,
1529 traits::{EntitySchema, FieldValue},
1530 types::{Account, Principal, Subaccount},
1531 value::{Value, ValueEnum},
1532 };
1533 use icydb_derive::{FieldProjection, PersistedRow};
1534 use serde::{Deserialize, Serialize};
1535
1536 crate::test_canister! {
1537 ident = PersistedRowPatchBridgeCanister,
1538 commit_memory_id = crate::testing::test_commit_memory_id(),
1539 }
1540
1541 crate::test_store! {
1542 ident = PersistedRowPatchBridgeStore,
1543 canister = PersistedRowPatchBridgeCanister,
1544 }
1545
1546 #[derive(
1558 Clone, Debug, Default, Deserialize, FieldProjection, PartialEq, PersistedRow, Serialize,
1559 )]
1560 struct PersistedRowPatchBridgeEntity {
1561 id: crate::types::Ulid,
1562 name: String,
1563 }
1564
1565 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1566 struct PersistedRowProfileValue {
1567 bio: String,
1568 }
1569
1570 impl FieldValue for PersistedRowProfileValue {
1571 fn kind() -> crate::traits::FieldValueKind {
1572 crate::traits::FieldValueKind::Structured { queryable: false }
1573 }
1574
1575 fn to_value(&self) -> Value {
1576 Value::from_map(vec![(
1577 Value::Text("bio".to_string()),
1578 Value::Text(self.bio.clone()),
1579 )])
1580 .expect("profile test value should encode as canonical map")
1581 }
1582
1583 fn from_value(value: &Value) -> Option<Self> {
1584 let Value::Map(entries) = value else {
1585 return None;
1586 };
1587 let normalized = Value::normalize_map_entries(entries.clone()).ok()?;
1588 let bio = normalized
1589 .iter()
1590 .find_map(|(entry_key, entry_value)| match entry_key {
1591 Value::Text(entry_key) if entry_key == "bio" => match entry_value {
1592 Value::Text(bio) => Some(bio.clone()),
1593 _ => None,
1594 },
1595 _ => None,
1596 })?;
1597
1598 if normalized.len() != 1 {
1599 return None;
1600 }
1601
1602 Some(Self { bio })
1603 }
1604 }
1605
1606 crate::test_entity_schema! {
1607 ident = PersistedRowPatchBridgeEntity,
1608 id = crate::types::Ulid,
1609 id_field = id,
1610 entity_name = "PersistedRowPatchBridgeEntity",
1611 entity_tag = SIMPLE_ENTITY_TAG,
1612 pk_index = 0,
1613 fields = [
1614 ("id", FieldKind::Ulid),
1615 ("name", FieldKind::Text),
1616 ],
1617 indexes = [],
1618 store = PersistedRowPatchBridgeStore,
1619 canister = PersistedRowPatchBridgeCanister,
1620 }
1621
1622 static STATE_VARIANTS: &[EnumVariantModel] = &[EnumVariantModel::new(
1623 "Loaded",
1624 Some(&FieldKind::Uint),
1625 FieldStorageDecode::ByKind,
1626 )];
1627 static FIELD_MODELS: [FieldModel; 2] = [
1628 FieldModel::new("name", FieldKind::Text),
1629 FieldModel::new_with_storage_decode("payload", FieldKind::Text, FieldStorageDecode::Value),
1630 ];
1631 static LIST_FIELD_MODELS: [FieldModel; 1] =
1632 [FieldModel::new("tags", FieldKind::List(&FieldKind::Text))];
1633 static MAP_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
1634 "props",
1635 FieldKind::Map {
1636 key: &FieldKind::Text,
1637 value: &FieldKind::Uint,
1638 },
1639 )];
1640 static ENUM_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
1641 "state",
1642 FieldKind::Enum {
1643 path: "tests::State",
1644 variants: STATE_VARIANTS,
1645 },
1646 )];
1647 static ACCOUNT_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new("owner", FieldKind::Account)];
1648 static REQUIRED_STRUCTURED_FIELD_MODELS: [FieldModel; 1] = [FieldModel::new(
1649 "profile",
1650 FieldKind::Structured { queryable: false },
1651 )];
1652 static OPTIONAL_STRUCTURED_FIELD_MODELS: [FieldModel; 1] =
1653 [FieldModel::new_with_storage_decode_and_nullability(
1654 "profile",
1655 FieldKind::Structured { queryable: false },
1656 FieldStorageDecode::ByKind,
1657 true,
1658 )];
1659 static STRUCTURED_MAP_VALUE_KIND: FieldKind = FieldKind::Structured { queryable: false };
1660 static STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS: [FieldModel; 1] =
1661 [FieldModel::new_with_storage_decode(
1662 "projects",
1663 FieldKind::Map {
1664 key: &FieldKind::Principal,
1665 value: &STRUCTURED_MAP_VALUE_KIND,
1666 },
1667 FieldStorageDecode::Value,
1668 )];
1669 static INDEX_MODELS: [&crate::model::index::IndexModel; 0] = [];
1670 static TEST_MODEL: EntityModel = EntityModel::new(
1671 "tests::PersistedRowFieldCodecEntity",
1672 "persisted_row_field_codec_entity",
1673 &FIELD_MODELS[0],
1674 &FIELD_MODELS,
1675 &INDEX_MODELS,
1676 );
1677 static LIST_MODEL: EntityModel = EntityModel::new(
1678 "tests::PersistedRowListFieldCodecEntity",
1679 "persisted_row_list_field_codec_entity",
1680 &LIST_FIELD_MODELS[0],
1681 &LIST_FIELD_MODELS,
1682 &INDEX_MODELS,
1683 );
1684 static MAP_MODEL: EntityModel = EntityModel::new(
1685 "tests::PersistedRowMapFieldCodecEntity",
1686 "persisted_row_map_field_codec_entity",
1687 &MAP_FIELD_MODELS[0],
1688 &MAP_FIELD_MODELS,
1689 &INDEX_MODELS,
1690 );
1691 static ENUM_MODEL: EntityModel = EntityModel::new(
1692 "tests::PersistedRowEnumFieldCodecEntity",
1693 "persisted_row_enum_field_codec_entity",
1694 &ENUM_FIELD_MODELS[0],
1695 &ENUM_FIELD_MODELS,
1696 &INDEX_MODELS,
1697 );
1698 static ACCOUNT_MODEL: EntityModel = EntityModel::new(
1699 "tests::PersistedRowAccountFieldCodecEntity",
1700 "persisted_row_account_field_codec_entity",
1701 &ACCOUNT_FIELD_MODELS[0],
1702 &ACCOUNT_FIELD_MODELS,
1703 &INDEX_MODELS,
1704 );
1705 static REQUIRED_STRUCTURED_MODEL: EntityModel = EntityModel::new(
1706 "tests::PersistedRowRequiredStructuredFieldCodecEntity",
1707 "persisted_row_required_structured_field_codec_entity",
1708 &REQUIRED_STRUCTURED_FIELD_MODELS[0],
1709 &REQUIRED_STRUCTURED_FIELD_MODELS,
1710 &INDEX_MODELS,
1711 );
1712 static OPTIONAL_STRUCTURED_MODEL: EntityModel = EntityModel::new(
1713 "tests::PersistedRowOptionalStructuredFieldCodecEntity",
1714 "persisted_row_optional_structured_field_codec_entity",
1715 &OPTIONAL_STRUCTURED_FIELD_MODELS[0],
1716 &OPTIONAL_STRUCTURED_FIELD_MODELS,
1717 &INDEX_MODELS,
1718 );
1719 static STRUCTURED_MAP_VALUE_STORAGE_MODEL: EntityModel = EntityModel::new(
1720 "tests::PersistedRowStructuredMapValueStorageEntity",
1721 "persisted_row_structured_map_value_storage_entity",
1722 &STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS[0],
1723 &STRUCTURED_MAP_VALUE_STORAGE_FIELD_MODELS,
1724 &INDEX_MODELS,
1725 );
1726
1727 fn encode_slot_payload_allowing_missing_for_tests(
1728 model: &'static EntityModel,
1729 slots: &[Option<&[u8]>],
1730 ) -> Result<Vec<u8>, InternalError> {
1731 if slots.len() != model.fields().len() {
1732 return Err(InternalError::persisted_row_encode_failed(format!(
1733 "noncanonical slot payload test helper expected {} slots for entity '{}', found {}",
1734 model.fields().len(),
1735 model.path(),
1736 slots.len()
1737 )));
1738 }
1739 let mut payload_bytes = Vec::new();
1740 let mut slot_table = Vec::with_capacity(slots.len());
1741
1742 for slot_payload in slots {
1743 match slot_payload {
1744 Some(bytes) => {
1745 let start = u32::try_from(payload_bytes.len()).map_err(|_| {
1746 InternalError::persisted_row_encode_failed(
1747 "slot payload start exceeds u32 range",
1748 )
1749 })?;
1750 let len = u32::try_from(bytes.len()).map_err(|_| {
1751 InternalError::persisted_row_encode_failed(
1752 "slot payload length exceeds u32 range",
1753 )
1754 })?;
1755 payload_bytes.extend_from_slice(bytes);
1756 slot_table.push((start, len));
1757 }
1758 None => slot_table.push((0_u32, 0_u32)),
1759 }
1760 }
1761
1762 encode_slot_payload_from_parts(slots.len(), slot_table.as_slice(), payload_bytes.as_slice())
1763 }
1764
1765 #[test]
1766 fn decode_slot_value_from_bytes_decodes_scalar_slots_through_one_owner() {
1767 let payload =
1768 encode_scalar_slot_value(ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")));
1769 let value =
1770 decode_slot_value_from_bytes(&TEST_MODEL, 0, payload.as_slice()).expect("decode slot");
1771
1772 assert_eq!(value, Value::Text("Ada".to_string()));
1773 }
1774
1775 #[test]
1776 fn decode_slot_value_from_bytes_reports_scalar_prefix_bytes() {
1777 let err = decode_slot_value_from_bytes(&TEST_MODEL, 0, &[0x00, 1])
1778 .expect_err("invalid scalar slot prefix should fail closed");
1779
1780 assert!(
1781 err.message
1782 .contains("expected slot envelope prefix byte 0xFF, found 0x00"),
1783 "unexpected error: {err:?}"
1784 );
1785 }
1786
1787 #[test]
1788 fn decode_slot_value_from_bytes_respects_value_storage_decode_contract() {
1789 let payload = crate::serialize::serialize(&Value::Text("Ada".to_string()))
1790 .expect("encode value-storage payload");
1791
1792 let value =
1793 decode_slot_value_from_bytes(&TEST_MODEL, 1, payload.as_slice()).expect("decode slot");
1794
1795 assert_eq!(value, Value::Text("Ada".to_string()));
1796 }
1797
1798 #[test]
1799 fn encode_slot_value_from_value_roundtrips_scalar_slots() {
1800 let payload = encode_slot_value_from_value(&TEST_MODEL, 0, &Value::Text("Ada".to_string()))
1801 .expect("encode slot");
1802 let decoded =
1803 decode_slot_value_from_bytes(&TEST_MODEL, 0, payload.as_slice()).expect("decode slot");
1804
1805 assert_eq!(decoded, Value::Text("Ada".to_string()));
1806 }
1807
1808 #[test]
1809 fn encode_slot_value_from_value_roundtrips_value_storage_slots() {
1810 let payload = encode_slot_value_from_value(&TEST_MODEL, 1, &Value::Text("Ada".to_string()))
1811 .expect("encode slot");
1812 let decoded =
1813 decode_slot_value_from_bytes(&TEST_MODEL, 1, payload.as_slice()).expect("decode slot");
1814
1815 assert_eq!(decoded, Value::Text("Ada".to_string()));
1816 }
1817
1818 #[test]
1819 fn encode_slot_value_from_value_roundtrips_list_by_kind_slots() {
1820 let payload = encode_slot_value_from_value(
1821 &LIST_MODEL,
1822 0,
1823 &Value::List(vec![Value::Text("alpha".to_string())]),
1824 )
1825 .expect("encode list slot");
1826 let decoded =
1827 decode_slot_value_from_bytes(&LIST_MODEL, 0, payload.as_slice()).expect("decode slot");
1828
1829 assert_eq!(decoded, Value::List(vec![Value::Text("alpha".to_string())]),);
1830 }
1831
1832 #[test]
1833 fn encode_slot_value_from_value_roundtrips_map_by_kind_slots() {
1834 let payload = encode_slot_value_from_value(
1835 &MAP_MODEL,
1836 0,
1837 &Value::Map(vec![(Value::Text("alpha".to_string()), Value::Uint(7))]),
1838 )
1839 .expect("encode map slot");
1840 let decoded =
1841 decode_slot_value_from_bytes(&MAP_MODEL, 0, payload.as_slice()).expect("decode slot");
1842
1843 assert_eq!(
1844 decoded,
1845 Value::Map(vec![(Value::Text("alpha".to_string()), Value::Uint(7))]),
1846 );
1847 }
1848
1849 #[test]
1850 fn encode_slot_value_from_value_accepts_value_storage_maps_with_structured_values() {
1851 let principal = Principal::dummy(7);
1852 let project = Value::from_map(vec![
1853 (Value::Text("pid".to_string()), Value::Principal(principal)),
1854 (
1855 Value::Text("status".to_string()),
1856 Value::Enum(ValueEnum::new(
1857 "Saved",
1858 Some("design::app::user::customise::project::ProjectStatus"),
1859 )),
1860 ),
1861 ])
1862 .expect("project value should normalize into a canonical map");
1863 let projects = Value::from_map(vec![(Value::Principal(principal), project)])
1864 .expect("outer map should normalize into a canonical map");
1865
1866 let payload =
1867 encode_slot_value_from_value(&STRUCTURED_MAP_VALUE_STORAGE_MODEL, 0, &projects)
1868 .expect("encode structured map slot");
1869 let decoded = decode_slot_value_from_bytes(
1870 &STRUCTURED_MAP_VALUE_STORAGE_MODEL,
1871 0,
1872 payload.as_slice(),
1873 )
1874 .expect("decode structured map slot");
1875
1876 assert_eq!(decoded, projects);
1877 }
1878
1879 #[test]
1880 fn encode_slot_value_from_value_roundtrips_enum_by_kind_slots() {
1881 let payload = encode_slot_value_from_value(
1882 &ENUM_MODEL,
1883 0,
1884 &Value::Enum(
1885 ValueEnum::new("Loaded", Some("tests::State")).with_payload(Value::Uint(7)),
1886 ),
1887 )
1888 .expect("encode enum slot");
1889 let decoded =
1890 decode_slot_value_from_bytes(&ENUM_MODEL, 0, payload.as_slice()).expect("decode slot");
1891
1892 assert_eq!(
1893 decoded,
1894 Value::Enum(
1895 ValueEnum::new("Loaded", Some("tests::State")).with_payload(Value::Uint(7,))
1896 ),
1897 );
1898 }
1899
1900 #[test]
1901 fn encode_slot_value_from_value_roundtrips_leaf_by_kind_wrapper_slots() {
1902 let account = Account::from_parts(Principal::dummy(7), Some(Subaccount::from([7_u8; 32])));
1903 let payload = encode_slot_value_from_value(&ACCOUNT_MODEL, 0, &Value::Account(account))
1904 .expect("encode account slot");
1905 let decoded = decode_slot_value_from_bytes(&ACCOUNT_MODEL, 0, payload.as_slice())
1906 .expect("decode slot");
1907
1908 assert_eq!(decoded, Value::Account(account));
1909 }
1910
1911 #[test]
1912 fn custom_slot_payload_roundtrips_structured_field_value() {
1913 let profile = PersistedRowProfileValue {
1914 bio: "Ada".to_string(),
1915 };
1916 let payload = encode_persisted_custom_slot_payload(&profile, "profile")
1917 .expect("encode custom structured payload");
1918 let decoded = decode_persisted_custom_slot_payload::<PersistedRowProfileValue>(
1919 payload.as_slice(),
1920 "profile",
1921 )
1922 .expect("decode custom structured payload");
1923
1924 assert_eq!(decoded, profile);
1925 assert_eq!(
1926 decode_persisted_slot_payload::<Value>(payload.as_slice(), "profile")
1927 .expect("decode raw value payload"),
1928 profile.to_value(),
1929 );
1930 }
1931
1932 #[test]
1933 fn custom_many_slot_payload_roundtrips_structured_value_lists() {
1934 let profiles = vec![
1935 PersistedRowProfileValue {
1936 bio: "Ada".to_string(),
1937 },
1938 PersistedRowProfileValue {
1939 bio: "Grace".to_string(),
1940 },
1941 ];
1942 let payload = encode_persisted_custom_many_slot_payload(profiles.as_slice(), "profiles")
1943 .expect("encode custom structured list payload");
1944 let decoded = decode_persisted_custom_many_slot_payload::<PersistedRowProfileValue>(
1945 payload.as_slice(),
1946 "profiles",
1947 )
1948 .expect("decode custom structured list payload");
1949
1950 assert_eq!(decoded, profiles);
1951 }
1952
1953 #[test]
1954 fn decode_persisted_non_null_slot_payload_rejects_null_for_required_structured_fields() {
1955 let err =
1956 decode_persisted_non_null_slot_payload::<PersistedRowProfileValue>(&[0xF6], "profile")
1957 .expect_err("required structured payload must reject null");
1958
1959 assert!(
1960 err.message
1961 .contains("unexpected null for non-nullable field"),
1962 "unexpected error: {err:?}"
1963 );
1964 }
1965
1966 #[test]
1967 fn decode_persisted_option_slot_payload_treats_cbor_null_as_none() {
1968 let decoded =
1969 decode_persisted_option_slot_payload::<PersistedRowProfileValue>(&[0xF6], "profile")
1970 .expect("optional structured payload should decode");
1971
1972 assert_eq!(decoded, None);
1973 }
1974
1975 #[test]
1976 fn encode_slot_value_from_value_rejects_null_for_required_structured_slots() {
1977 let err = encode_slot_value_from_value(&REQUIRED_STRUCTURED_MODEL, 0, &Value::Null)
1978 .expect_err("required structured slot must reject null");
1979
1980 assert!(
1981 err.message.contains("required field cannot store null"),
1982 "unexpected error: {err:?}"
1983 );
1984 }
1985
1986 #[test]
1987 fn encode_slot_value_from_value_allows_null_for_optional_structured_slots() {
1988 let payload = encode_slot_value_from_value(&OPTIONAL_STRUCTURED_MODEL, 0, &Value::Null)
1989 .expect("optional structured slot should allow null");
1990 let decoded =
1991 decode_slot_value_from_bytes(&OPTIONAL_STRUCTURED_MODEL, 0, payload.as_slice())
1992 .expect("optional structured slot should decode");
1993
1994 assert_eq!(decoded, Value::Null);
1995 }
1996
1997 #[test]
1998 fn encode_slot_value_from_value_rejects_unknown_enum_payload_variants() {
1999 let err = encode_slot_value_from_value(
2000 &ENUM_MODEL,
2001 0,
2002 &Value::Enum(
2003 ValueEnum::new("Unknown", Some("tests::State")).with_payload(Value::Uint(7)),
2004 ),
2005 )
2006 .expect_err("unknown enum payload should fail closed");
2007
2008 assert!(err.message.contains("unknown enum variant"));
2009 }
2010
2011 #[test]
2012 fn structural_slot_reader_and_direct_decode_share_the_same_field_codec_boundary() {
2013 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2014 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2015 .expect("encode value-storage payload");
2016 writer
2017 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2018 .expect("write scalar slot");
2019 writer
2020 .write_slot(1, Some(payload.as_slice()))
2021 .expect("write value-storage slot");
2022 let raw_row = RawRow::try_new(
2023 serialize_row_payload(writer.finish().expect("finish slot payload"))
2024 .expect("serialize row payload"),
2025 )
2026 .expect("build raw row");
2027
2028 let direct_slots =
2029 StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL).expect("decode row");
2030 let mut cached_slots =
2031 StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL).expect("decode row");
2032
2033 let direct_name = decode_slot_value_by_contract(&direct_slots, 0).expect("decode name");
2034 let direct_payload =
2035 decode_slot_value_by_contract(&direct_slots, 1).expect("decode payload");
2036 let cached_name = cached_slots.get_value(0).expect("cached name");
2037 let cached_payload = cached_slots.get_value(1).expect("cached payload");
2038
2039 assert_eq!(direct_name, cached_name);
2040 assert_eq!(direct_payload, cached_payload);
2041 }
2042
2043 #[test]
2044 fn apply_update_patch_to_raw_row_updates_only_targeted_slots() {
2045 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2046 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2047 .expect("encode value-storage payload");
2048 writer
2049 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2050 .expect("write scalar slot");
2051 writer
2052 .write_slot(1, Some(payload.as_slice()))
2053 .expect("write value-storage slot");
2054 let raw_row = RawRow::try_new(
2055 serialize_row_payload(writer.finish().expect("finish slot payload"))
2056 .expect("serialize row payload"),
2057 )
2058 .expect("build raw row");
2059 let patch = UpdatePatch::new().set(
2060 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2061 Value::Text("Grace".to_string()),
2062 );
2063
2064 let patched =
2065 apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch).expect("apply patch");
2066 let mut reader =
2067 StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
2068
2069 assert_eq!(
2070 reader.get_value(0).expect("decode slot"),
2071 Some(Value::Text("Grace".to_string()))
2072 );
2073 assert_eq!(
2074 reader.get_value(1).expect("decode slot"),
2075 Some(Value::Text("payload".to_string()))
2076 );
2077 }
2078
2079 #[test]
2080 fn serialize_update_patch_fields_encodes_canonical_slot_payloads() {
2081 let patch = UpdatePatch::new()
2082 .set(
2083 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2084 Value::Text("Grace".to_string()),
2085 )
2086 .set(
2087 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2088 Value::Text("payload".to_string()),
2089 );
2090
2091 let serialized =
2092 serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
2093
2094 assert_eq!(serialized.entries().len(), 2);
2095 assert_eq!(
2096 decode_slot_value_from_bytes(
2097 &TEST_MODEL,
2098 serialized.entries()[0].slot().index(),
2099 serialized.entries()[0].payload(),
2100 )
2101 .expect("decode slot payload"),
2102 Value::Text("Grace".to_string())
2103 );
2104 assert_eq!(
2105 decode_slot_value_from_bytes(
2106 &TEST_MODEL,
2107 serialized.entries()[1].slot().index(),
2108 serialized.entries()[1].payload(),
2109 )
2110 .expect("decode slot payload"),
2111 Value::Text("payload".to_string())
2112 );
2113 }
2114
2115 #[test]
2116 fn serialized_patch_writer_rejects_clear_slots() {
2117 let mut writer = SerializedPatchWriter::for_model(&TEST_MODEL);
2118
2119 let err = writer
2120 .write_slot(0, None)
2121 .expect_err("0.65 patch staging must reject missing-slot clears");
2122
2123 assert!(
2124 err.message
2125 .contains("serialized patch writer cannot clear slot 0"),
2126 "unexpected error: {err:?}"
2127 );
2128 assert!(
2129 err.message.contains(TEST_MODEL.path()),
2130 "unexpected error: {err:?}"
2131 );
2132 }
2133
2134 #[test]
2135 fn slot_buffer_writer_rejects_clear_slots() {
2136 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2137
2138 let err = writer
2139 .write_slot(0, None)
2140 .expect_err("canonical row staging must reject missing-slot clears");
2141
2142 assert!(
2143 err.message
2144 .contains("slot buffer writer cannot clear slot 0"),
2145 "unexpected error: {err:?}"
2146 );
2147 assert!(
2148 err.message.contains(TEST_MODEL.path()),
2149 "unexpected error: {err:?}"
2150 );
2151 }
2152
2153 #[test]
2154 fn apply_update_patch_to_raw_row_uses_last_write_wins() {
2155 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2156 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2157 .expect("encode value-storage payload");
2158 writer
2159 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2160 .expect("write scalar slot");
2161 writer
2162 .write_slot(1, Some(payload.as_slice()))
2163 .expect("write value-storage slot");
2164 let raw_row = RawRow::try_new(
2165 serialize_row_payload(writer.finish().expect("finish slot payload"))
2166 .expect("serialize row payload"),
2167 )
2168 .expect("build raw row");
2169 let slot = FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot");
2170 let patch = UpdatePatch::new()
2171 .set(slot, Value::Text("Grace".to_string()))
2172 .set(slot, Value::Text("Lin".to_string()));
2173
2174 let patched =
2175 apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch).expect("apply patch");
2176 let mut reader =
2177 StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
2178
2179 assert_eq!(
2180 reader.get_value(0).expect("decode slot"),
2181 Some(Value::Text("Lin".to_string()))
2182 );
2183 }
2184
2185 #[test]
2186 fn apply_update_patch_to_raw_row_rejects_noncanonical_missing_slot_baseline() {
2187 let empty_slots = vec![None::<&[u8]>; TEST_MODEL.fields().len()];
2188 let raw_row = RawRow::try_new(
2189 serialize_row_payload(
2190 encode_slot_payload_allowing_missing_for_tests(&TEST_MODEL, empty_slots.as_slice())
2191 .expect("encode malformed slot payload"),
2192 )
2193 .expect("serialize row payload"),
2194 )
2195 .expect("build raw row");
2196 let patch = UpdatePatch::new().set(
2197 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2198 Value::Text("payload".to_string()),
2199 );
2200
2201 let err = apply_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &patch)
2202 .expect_err("noncanonical rows with missing slots must fail closed");
2203
2204 assert_eq!(err.message, "row decode: missing slot payload: slot=0");
2205 }
2206
2207 #[test]
2208 fn apply_serialized_update_patch_to_raw_row_rejects_noncanonical_scalar_baseline() {
2209 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2210 .expect("encode value-storage payload");
2211 let malformed_slots = [Some([0xF6].as_slice()), Some(payload.as_slice())];
2212 let raw_row = RawRow::try_new(
2213 serialize_row_payload(
2214 encode_slot_payload_allowing_missing_for_tests(&TEST_MODEL, &malformed_slots)
2215 .expect("encode malformed slot payload"),
2216 )
2217 .expect("serialize row payload"),
2218 )
2219 .expect("build raw row");
2220 let patch = UpdatePatch::new().set(
2221 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2222 Value::Text("patched".to_string()),
2223 );
2224 let serialized =
2225 serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
2226
2227 let err = apply_serialized_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &serialized)
2228 .expect_err("noncanonical scalar baseline must fail closed");
2229
2230 assert!(
2231 err.message.contains("field 'name'"),
2232 "unexpected error: {err:?}"
2233 );
2234 assert!(
2235 err.message
2236 .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
2237 "unexpected error: {err:?}"
2238 );
2239 }
2240
2241 #[test]
2242 fn apply_serialized_update_patch_to_raw_row_rejects_noncanonical_scalar_patch_payload() {
2243 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2244 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2245 .expect("encode value-storage payload");
2246 writer
2247 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2248 .expect("write scalar slot");
2249 writer
2250 .write_slot(1, Some(payload.as_slice()))
2251 .expect("write value-storage slot");
2252 let raw_row = RawRow::try_new(
2253 serialize_row_payload(writer.finish().expect("finish slot payload"))
2254 .expect("serialize row payload"),
2255 )
2256 .expect("build raw row");
2257 let serialized = SerializedUpdatePatch::new(vec![SerializedFieldUpdate::new(
2258 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2259 vec![0xF6],
2260 )]);
2261
2262 let err = apply_serialized_update_patch_to_raw_row(&TEST_MODEL, &raw_row, &serialized)
2263 .expect_err("noncanonical serialized patch payload must fail closed");
2264
2265 assert!(
2266 err.message.contains("field 'name'"),
2267 "unexpected error: {err:?}"
2268 );
2269 assert!(
2270 err.message
2271 .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
2272 "unexpected error: {err:?}"
2273 );
2274 }
2275
2276 #[test]
2277 fn structural_slot_reader_rejects_slot_count_mismatch() {
2278 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2279 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2280 .expect("encode payload");
2281 writer
2282 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2283 .expect("write scalar slot");
2284 writer
2285 .write_slot(1, Some(payload.as_slice()))
2286 .expect("write payload slot");
2287 let mut payload = writer.finish().expect("finish slot payload");
2288 payload[..2].copy_from_slice(&1_u16.to_be_bytes());
2289 let raw_row =
2290 RawRow::try_new(serialize_row_payload(payload).expect("serialize row payload"))
2291 .expect("build raw row");
2292
2293 let err = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
2294 .err()
2295 .expect("slot-count drift must fail closed");
2296
2297 assert_eq!(
2298 err.message,
2299 "row decode: slot count mismatch: expected 2, found 1"
2300 );
2301 }
2302
2303 #[test]
2304 fn structural_slot_reader_rejects_slot_span_exceeds_payload_length() {
2305 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2306 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2307 .expect("encode payload");
2308 writer
2309 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2310 .expect("write scalar slot");
2311 writer
2312 .write_slot(1, Some(payload.as_slice()))
2313 .expect("write payload slot");
2314 let mut payload = writer.finish().expect("finish slot payload");
2315
2316 payload[14..18].copy_from_slice(&u32::MAX.to_be_bytes());
2319 let raw_row =
2320 RawRow::try_new(serialize_row_payload(payload).expect("serialize row payload"))
2321 .expect("build raw row");
2322
2323 let err = StructuralSlotReader::from_raw_row(&raw_row, &TEST_MODEL)
2324 .err()
2325 .expect("slot span drift must fail closed");
2326
2327 assert_eq!(err.message, "row decode: slot span exceeds payload length");
2328 }
2329
2330 #[test]
2331 fn apply_serialized_update_patch_to_raw_row_replays_preencoded_slots() {
2332 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2333 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2334 .expect("encode value-storage payload");
2335 writer
2336 .write_scalar(0, ScalarSlotValueRef::Value(ScalarValueRef::Text("Ada")))
2337 .expect("write scalar slot");
2338 writer
2339 .write_slot(1, Some(payload.as_slice()))
2340 .expect("write value-storage slot");
2341 let raw_row = RawRow::try_new(
2342 serialize_row_payload(writer.finish().expect("finish slot payload"))
2343 .expect("serialize row payload"),
2344 )
2345 .expect("build raw row");
2346 let patch = UpdatePatch::new().set(
2347 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2348 Value::Text("Grace".to_string()),
2349 );
2350 let serialized =
2351 serialize_update_patch_fields(&TEST_MODEL, &patch).expect("serialize patch");
2352
2353 let patched = raw_row
2354 .apply_serialized_update_patch(&TEST_MODEL, &serialized)
2355 .expect("apply serialized patch");
2356 let mut reader =
2357 StructuralSlotReader::from_raw_row(&patched, &TEST_MODEL).expect("decode row");
2358
2359 assert_eq!(
2360 reader.get_value(0).expect("decode slot"),
2361 Some(Value::Text("Grace".to_string()))
2362 );
2363 }
2364
2365 #[test]
2366 fn serialize_entity_slots_as_update_patch_replays_full_typed_after_image() {
2367 let old_entity = PersistedRowPatchBridgeEntity {
2368 id: crate::types::Ulid::from_u128(7),
2369 name: "Ada".to_string(),
2370 };
2371 let new_entity = PersistedRowPatchBridgeEntity {
2372 id: crate::types::Ulid::from_u128(7),
2373 name: "Grace".to_string(),
2374 };
2375 let raw_row = RawRow::from_entity(&old_entity).expect("encode old row");
2376 let old_decoded = raw_row
2377 .try_decode::<PersistedRowPatchBridgeEntity>()
2378 .expect("decode old entity");
2379 let serialized =
2380 serialize_entity_slots_as_update_patch(&new_entity).expect("serialize entity patch");
2381 let direct =
2382 RawRow::from_serialized_update_patch(PersistedRowPatchBridgeEntity::MODEL, &serialized)
2383 .expect("direct row emission should succeed");
2384
2385 let patched = raw_row
2386 .apply_serialized_update_patch(PersistedRowPatchBridgeEntity::MODEL, &serialized)
2387 .expect("apply serialized patch");
2388 let decoded = patched
2389 .try_decode::<PersistedRowPatchBridgeEntity>()
2390 .expect("decode patched entity");
2391
2392 assert_eq!(
2393 direct, patched,
2394 "fresh row emission and replayed full-image patch must converge on identical bytes",
2395 );
2396 assert_eq!(old_decoded, old_entity);
2397 assert_eq!(decoded, new_entity);
2398 }
2399
2400 #[test]
2401 fn canonical_row_from_raw_row_replays_canonical_full_image_bytes() {
2402 let entity = PersistedRowPatchBridgeEntity {
2403 id: crate::types::Ulid::from_u128(11),
2404 name: "Ada".to_string(),
2405 };
2406 let raw_row = RawRow::from_entity(&entity).expect("encode canonical row");
2407 let canonical =
2408 super::canonical_row_from_raw_row(PersistedRowPatchBridgeEntity::MODEL, &raw_row)
2409 .expect("canonical re-emission should succeed");
2410
2411 assert_eq!(
2412 canonical.as_bytes(),
2413 raw_row.as_bytes(),
2414 "canonical raw-row rebuild must preserve already canonical row bytes",
2415 );
2416 }
2417
2418 #[test]
2419 fn canonical_row_from_raw_row_rejects_noncanonical_scalar_payload() {
2420 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2421 .expect("encode value-storage payload");
2422 let mut writer = SlotBufferWriter::for_model(&TEST_MODEL);
2423 writer
2424 .write_slot(0, Some(&[0xF6]))
2425 .expect("write malformed scalar slot");
2426 writer
2427 .write_slot(1, Some(payload.as_slice()))
2428 .expect("write value-storage slot");
2429 let raw_row = RawRow::try_new(
2430 serialize_row_payload(writer.finish().expect("finish slot payload"))
2431 .expect("serialize malformed row"),
2432 )
2433 .expect("build malformed raw row");
2434
2435 let err = super::canonical_row_from_raw_row(&TEST_MODEL, &raw_row)
2436 .expect_err("canonical raw-row rebuild must reject malformed scalar payloads");
2437
2438 assert!(
2439 err.message.contains("field 'name'"),
2440 "unexpected error: {err:?}"
2441 );
2442 assert!(
2443 err.message
2444 .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
2445 "unexpected error: {err:?}"
2446 );
2447 }
2448
2449 #[test]
2450 fn raw_row_from_serialized_update_patch_rejects_noncanonical_scalar_payload() {
2451 let payload = crate::serialize::serialize(&Value::Text("payload".to_string()))
2452 .expect("encode value-storage payload");
2453 let serialized = SerializedUpdatePatch::new(vec![
2454 SerializedFieldUpdate::new(
2455 FieldSlot::from_index(&TEST_MODEL, 0).expect("resolve slot"),
2456 vec![0xF6],
2457 ),
2458 SerializedFieldUpdate::new(
2459 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2460 payload,
2461 ),
2462 ]);
2463
2464 let err = RawRow::from_serialized_update_patch(&TEST_MODEL, &serialized)
2465 .expect_err("fresh row emission must reject noncanonical serialized patch payloads");
2466
2467 assert!(
2468 err.message.contains("field 'name'"),
2469 "unexpected error: {err:?}"
2470 );
2471 assert!(
2472 err.message
2473 .contains("expected slot envelope prefix byte 0xFF, found 0xF6"),
2474 "unexpected error: {err:?}"
2475 );
2476 }
2477
2478 #[test]
2479 fn raw_row_from_serialized_update_patch_rejects_incomplete_slot_image() {
2480 let serialized = SerializedUpdatePatch::new(vec![SerializedFieldUpdate::new(
2481 FieldSlot::from_index(&TEST_MODEL, 1).expect("resolve slot"),
2482 crate::serialize::serialize(&Value::Text("payload".to_string()))
2483 .expect("encode value-storage payload"),
2484 )]);
2485
2486 let err = RawRow::from_serialized_update_patch(&TEST_MODEL, &serialized)
2487 .expect_err("fresh row emission must reject missing declared slots");
2488
2489 assert!(
2490 err.message.contains("serialized patch did not emit slot 0"),
2491 "unexpected error: {err:?}"
2492 );
2493 }
2494}