1use crate::{traits::RuntimeValueKind, types::EntityTag};
7#[cfg(test)]
8use crate::{types::Decimal, value::Value};
9#[cfg(test)]
10use std::{borrow::Cow, cmp::Ordering};
11
12#[cfg(test)]
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub(crate) enum FieldStorageValidationError {
15 RequiredFieldNull,
16 StorageContract,
17 DecimalScaleMismatch,
18 ScalarMaxLen,
19 SetCanonicalOrder,
20 MapEntryContract,
21 MapCanonicalOrder,
22}
23
24pub const DEFAULT_BIG_INT_MAX_BYTES: u32 = 256;
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum FieldStorageDecode {
39 ByKind,
41 Value,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum ScalarCodec {
56 Blob,
57 Bool,
58 Date,
59 Duration,
60 Float32,
61 Float64,
62 Int64,
63 Principal,
64 Subaccount,
65 Text,
66 Timestamp,
67 Nat64,
68 Ulid,
69 Unit,
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub enum LeafCodec {
83 Scalar(ScalarCodec),
84 StructuralFallback,
85}
86
87#[derive(Clone, Copy, Debug)]
97pub struct EnumVariantModel {
98 pub(crate) ident: &'static str,
100 pub(crate) payload_kind: Option<&'static FieldKind>,
102 pub(crate) payload_storage_decode: FieldStorageDecode,
104}
105
106impl EnumVariantModel {
107 #[must_use]
109 pub const fn new(
110 ident: &'static str,
111 payload_kind: Option<&'static FieldKind>,
112 payload_storage_decode: FieldStorageDecode,
113 ) -> Self {
114 Self {
115 ident,
116 payload_kind,
117 payload_storage_decode,
118 }
119 }
120
121 #[must_use]
123 pub const fn ident(&self) -> &'static str {
124 self.ident
125 }
126
127 #[must_use]
129 pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
130 self.payload_kind
131 }
132
133 #[must_use]
135 pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
136 self.payload_storage_decode
137 }
138}
139
140#[derive(Debug)]
150pub struct FieldModel {
151 pub(crate) name: &'static str,
153 pub(crate) kind: FieldKind,
155 pub(crate) nested_fields: &'static [Self],
157 pub(crate) nullable: bool,
159 pub(crate) storage_decode: FieldStorageDecode,
161 pub(crate) leaf_codec: LeafCodec,
163 pub(crate) insert_generation: Option<FieldInsertGeneration>,
165 pub(crate) write_management: Option<FieldWriteManagement>,
167 pub(crate) database_default: FieldDatabaseDefault,
169}
170
171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
180pub enum FieldDatabaseDefault {
181 None,
183 EncodedSlotPayload(&'static [u8]),
189 AuthoredEnumUnit {
194 enum_path: &'static str,
196 variant: &'static str,
198 },
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub enum FieldInsertGeneration {
212 Ulid,
214 Timestamp,
216}
217
218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228pub enum FieldWriteManagement {
229 CreatedAt,
231 UpdatedAt,
233}
234
235impl FieldModel {
236 #[must_use]
242 #[doc(hidden)]
243 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
244 Self::generated_with_storage_decode_and_nullability(
245 name,
246 kind,
247 FieldStorageDecode::ByKind,
248 false,
249 )
250 }
251
252 #[must_use]
254 #[doc(hidden)]
255 pub const fn generated_with_storage_decode(
256 name: &'static str,
257 kind: FieldKind,
258 storage_decode: FieldStorageDecode,
259 ) -> Self {
260 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
261 }
262
263 #[must_use]
265 #[doc(hidden)]
266 pub const fn generated_with_storage_decode_and_nullability(
267 name: &'static str,
268 kind: FieldKind,
269 storage_decode: FieldStorageDecode,
270 nullable: bool,
271 ) -> Self {
272 Self::generated_with_storage_decode_nullability_and_write_policies(
273 name,
274 kind,
275 storage_decode,
276 nullable,
277 None,
278 None,
279 )
280 }
281
282 #[must_use]
285 #[doc(hidden)]
286 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
287 name: &'static str,
288 kind: FieldKind,
289 storage_decode: FieldStorageDecode,
290 nullable: bool,
291 insert_generation: Option<FieldInsertGeneration>,
292 ) -> Self {
293 Self::generated_with_storage_decode_nullability_and_write_policies(
294 name,
295 kind,
296 storage_decode,
297 nullable,
298 insert_generation,
299 None,
300 )
301 }
302
303 #[must_use]
306 #[doc(hidden)]
307 pub const fn generated_with_storage_decode_nullability_and_write_policies(
308 name: &'static str,
309 kind: FieldKind,
310 storage_decode: FieldStorageDecode,
311 nullable: bool,
312 insert_generation: Option<FieldInsertGeneration>,
313 write_management: Option<FieldWriteManagement>,
314 ) -> Self {
315 Self {
316 name,
317 kind,
318 nested_fields: &[],
319 nullable,
320 storage_decode,
321 leaf_codec: leaf_codec_for(kind, storage_decode),
322 insert_generation,
323 write_management,
324 database_default: FieldDatabaseDefault::None,
325 }
326 }
327
328 #[must_use]
330 #[doc(hidden)]
331 pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
332 name: &'static str,
333 kind: FieldKind,
334 storage_decode: FieldStorageDecode,
335 nullable: bool,
336 insert_generation: Option<FieldInsertGeneration>,
337 write_management: Option<FieldWriteManagement>,
338 nested_fields: &'static [Self],
339 ) -> Self {
340 Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
341 name,
342 kind,
343 storage_decode,
344 nullable,
345 insert_generation,
346 write_management,
347 FieldDatabaseDefault::None,
348 nested_fields,
349 )
350 }
351
352 #[must_use]
355 #[doc(hidden)]
356 #[expect(
357 clippy::too_many_arguments,
358 reason = "generated schema metadata keeps every field contract explicit"
359 )]
360 pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
361 name: &'static str,
362 kind: FieldKind,
363 storage_decode: FieldStorageDecode,
364 nullable: bool,
365 insert_generation: Option<FieldInsertGeneration>,
366 write_management: Option<FieldWriteManagement>,
367 database_default: FieldDatabaseDefault,
368 nested_fields: &'static [Self],
369 ) -> Self {
370 Self {
371 name,
372 kind,
373 nested_fields,
374 nullable,
375 storage_decode,
376 leaf_codec: leaf_codec_for(kind, storage_decode),
377 insert_generation,
378 write_management,
379 database_default,
380 }
381 }
382
383 #[must_use]
385 pub const fn name(&self) -> &'static str {
386 self.name
387 }
388
389 #[must_use]
391 pub const fn kind(&self) -> FieldKind {
392 self.kind
393 }
394
395 #[must_use]
397 pub const fn nested_fields(&self) -> &'static [Self] {
398 self.nested_fields
399 }
400
401 #[must_use]
403 pub const fn nullable(&self) -> bool {
404 self.nullable
405 }
406
407 #[must_use]
409 pub const fn storage_decode(&self) -> FieldStorageDecode {
410 self.storage_decode
411 }
412
413 #[must_use]
415 pub const fn leaf_codec(&self) -> LeafCodec {
416 self.leaf_codec
417 }
418
419 #[must_use]
421 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
422 self.insert_generation
423 }
424
425 #[must_use]
427 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
428 self.write_management
429 }
430
431 #[must_use]
433 pub const fn database_default(&self) -> FieldDatabaseDefault {
434 self.database_default
435 }
436
437 #[cfg(test)]
445 pub(crate) fn validate_runtime_value_for_storage(
446 &self,
447 value: &Value,
448 ) -> Result<(), FieldStorageValidationError> {
449 if matches!(value, Value::Null) {
450 if self.nullable() {
451 return Ok(());
452 }
453
454 return Err(FieldStorageValidationError::RequiredFieldNull);
455 }
456
457 let accepts = match self.storage_decode() {
458 FieldStorageDecode::Value => {
459 value_storage_kind_accepts_runtime_value(self.kind(), value)
460 }
461 FieldStorageDecode::ByKind => {
462 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
463 }
464 };
465 if !accepts {
466 return Err(FieldStorageValidationError::StorageContract);
467 }
468
469 ensure_decimal_scale_matches(self.kind(), value)?;
470 ensure_scalar_max_len_matches(self.kind(), value)?;
471 ensure_value_is_deterministic_for_storage(self.kind(), value)
472 }
473
474 #[cfg(test)]
478 pub(crate) fn normalize_runtime_value_for_storage<'a>(
479 &self,
480 value: &'a Value,
481 ) -> Result<Cow<'a, Value>, FieldStorageValidationError> {
482 normalize_decimal_scale_for_storage(self.kind(), value)
483 }
484}
485
486const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
490 if matches!(storage_decode, FieldStorageDecode::Value) {
491 return LeafCodec::StructuralFallback;
492 }
493
494 match kind {
495 FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
496 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
497 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
498 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
499 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
500 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
501 FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
502 LeafCodec::Scalar(ScalarCodec::Int64)
503 }
504 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
505 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
506 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
507 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
508 FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
509 LeafCodec::Scalar(ScalarCodec::Nat64)
510 }
511 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
512 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
513 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
514 FieldKind::Account
515 | FieldKind::Decimal { .. }
516 | FieldKind::Enum { .. }
517 | FieldKind::Int128
518 | FieldKind::IntBig { .. }
519 | FieldKind::List(_)
520 | FieldKind::Map { .. }
521 | FieldKind::Set(_)
522 | FieldKind::Structured { .. }
523 | FieldKind::Nat128
524 | FieldKind::NatBig { .. } => LeafCodec::StructuralFallback,
525 }
526}
527
528#[derive(Clone, Copy, Debug, Eq, PartialEq)]
535pub enum RelationStrength {
536 Strong,
537 Weak,
538}
539
540#[derive(Clone, Copy, Debug)]
550pub enum FieldKind {
551 Account,
553 Blob {
554 max_len: Option<u32>,
556 },
557 Bool,
558 Date,
559 Decimal {
560 scale: u32,
562 },
563 Duration,
564 Enum {
565 path: &'static str,
567 variants: &'static [EnumVariantModel],
569 },
570 Float32,
571 Float64,
572 Int8,
573 Int16,
574 Int32,
575 Int64,
576 Int128,
577 IntBig {
578 max_bytes: u32,
580 },
581 Principal,
582 Subaccount,
583 Text {
584 max_len: Option<u32>,
586 },
587 Timestamp,
588 Nat8,
589 Nat16,
590 Nat32,
591 Nat64,
592 Nat128,
593 NatBig {
594 max_bytes: u32,
596 },
597 Ulid,
598 Unit,
599
600 Relation {
603 target_path: &'static str,
605 target_entity_name: &'static str,
607 target_entity_tag: EntityTag,
609 target_store_path: &'static str,
611 key_kind: &'static Self,
612 strength: RelationStrength,
613 },
614
615 List(&'static Self),
617 Set(&'static Self),
618 Map {
622 key: &'static Self,
623 value: &'static Self,
624 },
625
626 Structured {
630 queryable: bool,
631 },
632}
633
634impl FieldKind {
635 #[must_use]
636 pub const fn value_kind(&self) -> RuntimeValueKind {
637 match self {
638 Self::Account
639 | Self::Blob { .. }
640 | Self::Bool
641 | Self::Date
642 | Self::Duration
643 | Self::Enum { .. }
644 | Self::Float32
645 | Self::Float64
646 | Self::Int8
647 | Self::Int16
648 | Self::Int32
649 | Self::Int64
650 | Self::Int128
651 | Self::IntBig { .. }
652 | Self::Principal
653 | Self::Subaccount
654 | Self::Text { .. }
655 | Self::Timestamp
656 | Self::Nat8
657 | Self::Nat16
658 | Self::Nat32
659 | Self::Nat64
660 | Self::Nat128
661 | Self::NatBig { .. }
662 | Self::Ulid
663 | Self::Unit
664 | Self::Decimal { .. }
665 | Self::Relation { .. } => RuntimeValueKind::Atomic,
666 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
667 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
668 Self::Structured { queryable } => RuntimeValueKind::Structured {
669 queryable: *queryable,
670 },
671 }
672 }
673
674 #[must_use]
682 pub const fn is_deterministic_collection_shape(&self) -> bool {
683 match self {
684 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
685
686 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
687
688 Self::Map { key, value } => {
689 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
690 }
691
692 _ => true,
693 }
694 }
695
696 #[cfg(test)]
699 #[must_use]
700 pub(crate) fn supports_group_probe(&self) -> bool {
701 match self {
702 Self::Enum { variants, .. } => variants.iter().all(|variant| {
703 variant
704 .payload_kind()
705 .is_none_or(Self::supports_group_probe)
706 }),
707 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
708 Self::Decimal { .. } => true,
709 _ => super::field_kind_semantics::field_kind_has_identity_group_canonical_form(*self),
710 }
711 }
712
713 #[cfg(test)]
715 #[must_use]
716 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
717 match (self, value) {
718 (Self::Account, Value::Account(_))
719 | (Self::Blob { .. }, Value::Blob(_))
720 | (Self::Bool, Value::Bool(_))
721 | (Self::Date, Value::Date(_))
722 | (Self::Decimal { .. }, Value::Decimal(_))
723 | (Self::Duration, Value::Duration(_))
724 | (Self::Enum { .. }, Value::Enum(_))
725 | (Self::Float32, Value::Float32(_))
726 | (Self::Float64, Value::Float64(_))
727 | (Self::Int64, Value::Int64(_))
728 | (Self::Int128, Value::Int128(_))
729 | (Self::Nat64, Value::Nat64(_))
730 | (Self::Principal, Value::Principal(_))
731 | (Self::Subaccount, Value::Subaccount(_))
732 | (Self::Text { .. }, Value::Text(_))
733 | (Self::Timestamp, Value::Timestamp(_))
734 | (Self::Nat128, Value::Nat128(_))
735 | (Self::Ulid, Value::Ulid(_))
736 | (Self::Unit, Value::Unit)
737 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
738 (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
739 (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
740 (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
741 (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
742 (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
743 (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
744 (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
745 value.to_leb128().len() <= *max_bytes as usize
746 }
747 (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
748 value.to_leb128().len() <= *max_bytes as usize
749 }
750 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
751 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
752 items.iter().all(|item| inner.accepts_value(item))
753 }
754 (Self::Map { key, value }, Value::Map(entries)) => {
755 Value::validate_map_entries(entries.as_slice()).is_ok()
756 && entries.iter().all(|(entry_key, entry_value)| {
757 key.accepts_value(entry_key) && value.accepts_value(entry_value)
758 })
759 }
760 _ => false,
761 }
762 }
763}
764
765#[cfg(test)]
771fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
772 match (kind, value) {
773 (FieldKind::Relation { key_kind, .. }, value) => {
774 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
775 }
776 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
777 .iter()
778 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
779 (
780 FieldKind::Map {
781 key,
782 value: value_kind,
783 },
784 Value::Map(entries),
785 ) => {
786 if Value::validate_map_entries(entries.as_slice()).is_err() {
787 return false;
788 }
789
790 entries.iter().all(|(entry_key, entry_value)| {
791 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
792 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
793 })
794 }
795 (FieldKind::Structured { .. }, _) => false,
796 _ => kind.accepts_value(value),
797 }
798}
799
800#[cfg(test)]
804fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
805 match (kind, value) {
806 (FieldKind::Structured { .. }, _) => true,
807 (FieldKind::Relation { key_kind, .. }, value) => {
808 value_storage_kind_accepts_runtime_value(*key_kind, value)
809 }
810 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
811 .iter()
812 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
813 (
814 FieldKind::Map {
815 key,
816 value: value_kind,
817 },
818 Value::Map(entries),
819 ) => {
820 if Value::validate_map_entries(entries.as_slice()).is_err() {
821 return false;
822 }
823
824 entries.iter().all(|(entry_key, entry_value)| {
825 value_storage_kind_accepts_runtime_value(*key, entry_key)
826 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
827 })
828 }
829 _ => kind.accepts_value(value),
830 }
831}
832
833#[cfg(test)]
836fn ensure_decimal_scale_matches(
837 kind: FieldKind,
838 value: &Value,
839) -> Result<(), FieldStorageValidationError> {
840 if matches!(value, Value::Null) {
841 return Ok(());
842 }
843
844 match (kind, value) {
845 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
846 if decimal.scale() != scale {
847 return Err(FieldStorageValidationError::DecimalScaleMismatch);
848 }
849
850 Ok(())
851 }
852 (FieldKind::Relation { key_kind, .. }, value) => {
853 ensure_decimal_scale_matches(*key_kind, value)
854 }
855 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
856 for item in items {
857 ensure_decimal_scale_matches(*inner, item)?;
858 }
859
860 Ok(())
861 }
862 (
863 FieldKind::Map {
864 key,
865 value: map_value,
866 },
867 Value::Map(entries),
868 ) => {
869 for (entry_key, entry_value) in entries {
870 ensure_decimal_scale_matches(*key, entry_key)?;
871 ensure_decimal_scale_matches(*map_value, entry_value)?;
872 }
873
874 Ok(())
875 }
876 _ => Ok(()),
877 }
878}
879
880#[cfg(test)]
885pub(crate) fn normalize_decimal_scale_for_storage(
886 kind: FieldKind,
887 value: &Value,
888) -> Result<Cow<'_, Value>, FieldStorageValidationError> {
889 if matches!(value, Value::Null) {
890 return Ok(Cow::Borrowed(value));
891 }
892
893 match (kind, value) {
894 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
895 let normalized = decimal_with_storage_scale(*decimal, scale)
896 .ok_or(FieldStorageValidationError::DecimalScaleMismatch)?;
897
898 if normalized.scale() == decimal.scale() {
899 Ok(Cow::Borrowed(value))
900 } else {
901 Ok(Cow::Owned(Value::Decimal(normalized)))
902 }
903 }
904 (FieldKind::Relation { key_kind, .. }, value) => {
905 normalize_decimal_scale_for_storage(*key_kind, value)
906 }
907 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
908 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
909 items.map_or_else(
910 || Cow::Borrowed(value),
911 |items| Cow::Owned(Value::List(items)),
912 )
913 })
914 }
915 (
916 FieldKind::Map {
917 key,
918 value: map_value,
919 },
920 Value::Map(entries),
921 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
922 entries.map_or_else(
923 || Cow::Borrowed(value),
924 |entries| Cow::Owned(Value::Map(entries)),
925 )
926 }),
927 _ => Ok(Cow::Borrowed(value)),
928 }
929}
930
931#[cfg(test)]
935fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
936 match decimal.scale().cmp(&scale) {
937 Ordering::Equal => Some(decimal),
938 Ordering::Less => decimal
939 .scale_to_integer(scale)
940 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
941 Ordering::Greater => Some(decimal.round_dp(scale)),
942 }
943}
944
945#[cfg(test)]
948fn normalize_decimal_list_items(
949 kind: FieldKind,
950 items: &[Value],
951) -> Result<Option<Vec<Value>>, FieldStorageValidationError> {
952 let mut normalized_items = None;
953
954 for (index, item) in items.iter().enumerate() {
955 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
956 if let Cow::Owned(value) = normalized {
957 let items = normalized_items.get_or_insert_with(|| items.to_vec());
958 items[index] = value;
959 }
960 }
961
962 Ok(normalized_items)
963}
964
965#[cfg(test)]
968fn normalize_decimal_map_entries(
969 key_kind: FieldKind,
970 value_kind: FieldKind,
971 entries: &[(Value, Value)],
972) -> Result<Option<Vec<(Value, Value)>>, FieldStorageValidationError> {
973 let mut normalized_entries = None;
974
975 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
976 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
977 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
978
979 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
980 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
981 if let Cow::Owned(value) = normalized_key {
982 entries[index].0 = value;
983 }
984 if let Cow::Owned(value) = normalized_value {
985 entries[index].1 = value;
986 }
987 }
988 }
989
990 Ok(normalized_entries)
991}
992
993#[cfg(test)]
996fn ensure_scalar_max_len_matches(
997 kind: FieldKind,
998 value: &Value,
999) -> Result<(), FieldStorageValidationError> {
1000 if matches!(value, Value::Null) {
1001 return Ok(());
1002 }
1003
1004 match (kind, value) {
1005 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
1006 let len = text.chars().count();
1007 if len > max as usize {
1008 return Err(FieldStorageValidationError::ScalarMaxLen);
1009 }
1010
1011 Ok(())
1012 }
1013 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
1014 let len = bytes.len();
1015 if len > max as usize {
1016 return Err(FieldStorageValidationError::ScalarMaxLen);
1017 }
1018
1019 Ok(())
1020 }
1021 (FieldKind::Relation { key_kind, .. }, value) => {
1022 ensure_scalar_max_len_matches(*key_kind, value)
1023 }
1024 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1025 for item in items {
1026 ensure_scalar_max_len_matches(*inner, item)?;
1027 }
1028
1029 Ok(())
1030 }
1031 (
1032 FieldKind::Map {
1033 key,
1034 value: map_value,
1035 },
1036 Value::Map(entries),
1037 ) => {
1038 for (entry_key, entry_value) in entries {
1039 ensure_scalar_max_len_matches(*key, entry_key)?;
1040 ensure_scalar_max_len_matches(*map_value, entry_value)?;
1041 }
1042
1043 Ok(())
1044 }
1045 _ => Ok(()),
1046 }
1047}
1048
1049#[cfg(test)]
1052fn ensure_value_is_deterministic_for_storage(
1053 kind: FieldKind,
1054 value: &Value,
1055) -> Result<(), FieldStorageValidationError> {
1056 match (kind, value) {
1057 (FieldKind::Set(_), Value::List(items)) => {
1058 for pair in items.windows(2) {
1059 let [left, right] = pair else {
1060 continue;
1061 };
1062 if Value::canonical_cmp(left, right) != Ordering::Less {
1063 return Err(FieldStorageValidationError::SetCanonicalOrder);
1064 }
1065 }
1066
1067 Ok(())
1068 }
1069 (FieldKind::Map { .. }, Value::Map(entries)) => {
1070 Value::validate_map_entries(entries.as_slice())
1071 .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
1072
1073 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1074 return Err(FieldStorageValidationError::MapCanonicalOrder);
1075 }
1076
1077 Ok(())
1078 }
1079 _ => Ok(()),
1080 }
1081}
1082
1083#[cfg(test)]
1088mod tests {
1089 use crate::{
1090 model::field::{FieldKind, FieldModel},
1091 value::Value,
1092 };
1093
1094 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1095 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1096
1097 #[test]
1098 fn text_max_len_accepts_unbounded_text() {
1099 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1100
1101 assert!(
1102 field
1103 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1104 .is_ok()
1105 );
1106 }
1107
1108 #[test]
1109 fn text_max_len_counts_unicode_scalars_not_bytes() {
1110 let field = FieldModel::generated("name", BOUNDED_TEXT);
1111
1112 assert!(
1113 field
1114 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1115 .is_ok()
1116 );
1117 assert!(
1118 field
1119 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1120 .is_err()
1121 );
1122 }
1123
1124 #[test]
1125 fn text_max_len_recurses_through_collections() {
1126 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1127 static TEXT_MAP: FieldKind = FieldKind::Map {
1128 key: &BOUNDED_TEXT,
1129 value: &BOUNDED_TEXT,
1130 };
1131
1132 let list_field = FieldModel::generated("names", TEXT_LIST);
1133 let map_field = FieldModel::generated("labels", TEXT_MAP);
1134
1135 assert!(
1136 list_field
1137 .validate_runtime_value_for_storage(&Value::List(vec![
1138 Value::Text("Ada".into()),
1139 Value::Text("Bob".into()),
1140 ]))
1141 .is_ok()
1142 );
1143 assert!(
1144 list_field
1145 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1146 .is_err()
1147 );
1148 assert!(
1149 map_field
1150 .validate_runtime_value_for_storage(&Value::Map(vec![(
1151 Value::Text("key".into()),
1152 Value::Text("val".into()),
1153 )]))
1154 .is_ok()
1155 );
1156 assert!(
1157 map_field
1158 .validate_runtime_value_for_storage(&Value::Map(vec![(
1159 Value::Text("long".into()),
1160 Value::Text("val".into()),
1161 )]))
1162 .is_err()
1163 );
1164 }
1165
1166 #[test]
1167 fn blob_max_len_counts_bytes() {
1168 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1169
1170 assert!(
1171 field
1172 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1173 .is_ok()
1174 );
1175 assert!(
1176 field
1177 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1178 .is_err()
1179 );
1180 }
1181
1182 #[test]
1183 fn blob_max_len_recurses_through_collections() {
1184 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1185
1186 let field = FieldModel::generated("payloads", BLOB_LIST);
1187
1188 assert!(
1189 field
1190 .validate_runtime_value_for_storage(&Value::List(vec![
1191 Value::Blob(vec![1, 2, 3]),
1192 Value::Blob(vec![4, 5]),
1193 ]))
1194 .is_ok()
1195 );
1196 assert!(
1197 field
1198 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1199 1, 2, 3, 4
1200 ])]))
1201 .is_err()
1202 );
1203 }
1204}