1use crate::{
7 traits::RuntimeValueKind,
8 types::{Decimal, EntityTag},
9 value::Value,
10};
11use std::{borrow::Cow, cmp::Ordering};
12
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}
190
191#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub enum FieldInsertGeneration {
202 Ulid,
204 Timestamp,
206}
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218pub enum FieldWriteManagement {
219 CreatedAt,
221 UpdatedAt,
223}
224
225impl FieldModel {
226 #[must_use]
232 #[doc(hidden)]
233 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
234 Self::generated_with_storage_decode_and_nullability(
235 name,
236 kind,
237 FieldStorageDecode::ByKind,
238 false,
239 )
240 }
241
242 #[must_use]
244 #[doc(hidden)]
245 pub const fn generated_with_storage_decode(
246 name: &'static str,
247 kind: FieldKind,
248 storage_decode: FieldStorageDecode,
249 ) -> Self {
250 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
251 }
252
253 #[must_use]
255 #[doc(hidden)]
256 pub const fn generated_with_storage_decode_and_nullability(
257 name: &'static str,
258 kind: FieldKind,
259 storage_decode: FieldStorageDecode,
260 nullable: bool,
261 ) -> Self {
262 Self::generated_with_storage_decode_nullability_and_write_policies(
263 name,
264 kind,
265 storage_decode,
266 nullable,
267 None,
268 None,
269 )
270 }
271
272 #[must_use]
275 #[doc(hidden)]
276 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
277 name: &'static str,
278 kind: FieldKind,
279 storage_decode: FieldStorageDecode,
280 nullable: bool,
281 insert_generation: Option<FieldInsertGeneration>,
282 ) -> Self {
283 Self::generated_with_storage_decode_nullability_and_write_policies(
284 name,
285 kind,
286 storage_decode,
287 nullable,
288 insert_generation,
289 None,
290 )
291 }
292
293 #[must_use]
296 #[doc(hidden)]
297 pub const fn generated_with_storage_decode_nullability_and_write_policies(
298 name: &'static str,
299 kind: FieldKind,
300 storage_decode: FieldStorageDecode,
301 nullable: bool,
302 insert_generation: Option<FieldInsertGeneration>,
303 write_management: Option<FieldWriteManagement>,
304 ) -> Self {
305 Self {
306 name,
307 kind,
308 nested_fields: &[],
309 nullable,
310 storage_decode,
311 leaf_codec: leaf_codec_for(kind, storage_decode),
312 insert_generation,
313 write_management,
314 database_default: FieldDatabaseDefault::None,
315 }
316 }
317
318 #[must_use]
320 #[doc(hidden)]
321 pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
322 name: &'static str,
323 kind: FieldKind,
324 storage_decode: FieldStorageDecode,
325 nullable: bool,
326 insert_generation: Option<FieldInsertGeneration>,
327 write_management: Option<FieldWriteManagement>,
328 nested_fields: &'static [Self],
329 ) -> Self {
330 Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
331 name,
332 kind,
333 storage_decode,
334 nullable,
335 insert_generation,
336 write_management,
337 FieldDatabaseDefault::None,
338 nested_fields,
339 )
340 }
341
342 #[must_use]
345 #[doc(hidden)]
346 #[expect(
347 clippy::too_many_arguments,
348 reason = "generated schema metadata keeps every field contract explicit"
349 )]
350 pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
351 name: &'static str,
352 kind: FieldKind,
353 storage_decode: FieldStorageDecode,
354 nullable: bool,
355 insert_generation: Option<FieldInsertGeneration>,
356 write_management: Option<FieldWriteManagement>,
357 database_default: FieldDatabaseDefault,
358 nested_fields: &'static [Self],
359 ) -> Self {
360 Self {
361 name,
362 kind,
363 nested_fields,
364 nullable,
365 storage_decode,
366 leaf_codec: leaf_codec_for(kind, storage_decode),
367 insert_generation,
368 write_management,
369 database_default,
370 }
371 }
372
373 #[must_use]
375 pub const fn name(&self) -> &'static str {
376 self.name
377 }
378
379 #[must_use]
381 pub const fn kind(&self) -> FieldKind {
382 self.kind
383 }
384
385 #[must_use]
387 pub const fn nested_fields(&self) -> &'static [Self] {
388 self.nested_fields
389 }
390
391 #[must_use]
393 pub const fn nullable(&self) -> bool {
394 self.nullable
395 }
396
397 #[must_use]
399 pub const fn storage_decode(&self) -> FieldStorageDecode {
400 self.storage_decode
401 }
402
403 #[must_use]
405 pub const fn leaf_codec(&self) -> LeafCodec {
406 self.leaf_codec
407 }
408
409 #[must_use]
411 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
412 self.insert_generation
413 }
414
415 #[must_use]
417 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
418 self.write_management
419 }
420
421 #[must_use]
423 pub const fn database_default(&self) -> FieldDatabaseDefault {
424 self.database_default
425 }
426
427 pub(crate) fn validate_runtime_value_for_storage(
435 &self,
436 value: &Value,
437 ) -> Result<(), FieldStorageValidationError> {
438 if matches!(value, Value::Null) {
439 if self.nullable() {
440 return Ok(());
441 }
442
443 return Err(FieldStorageValidationError::RequiredFieldNull);
444 }
445
446 let accepts = match self.storage_decode() {
447 FieldStorageDecode::Value => {
448 value_storage_kind_accepts_runtime_value(self.kind(), value)
449 }
450 FieldStorageDecode::ByKind => {
451 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
452 }
453 };
454 if !accepts {
455 return Err(FieldStorageValidationError::StorageContract);
456 }
457
458 ensure_decimal_scale_matches(self.kind(), value)?;
459 ensure_scalar_max_len_matches(self.kind(), value)?;
460 ensure_value_is_deterministic_for_storage(self.kind(), value)
461 }
462
463 pub(crate) fn normalize_runtime_value_for_storage<'a>(
467 &self,
468 value: &'a Value,
469 ) -> Result<Cow<'a, Value>, FieldStorageValidationError> {
470 normalize_decimal_scale_for_storage(self.kind(), value)
471 }
472}
473
474const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
478 if matches!(storage_decode, FieldStorageDecode::Value) {
479 return LeafCodec::StructuralFallback;
480 }
481
482 match kind {
483 FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
484 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
485 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
486 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
487 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
488 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
489 FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
490 LeafCodec::Scalar(ScalarCodec::Int64)
491 }
492 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
493 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
494 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
495 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
496 FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
497 LeafCodec::Scalar(ScalarCodec::Nat64)
498 }
499 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
500 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
501 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
502 FieldKind::Account
503 | FieldKind::Decimal { .. }
504 | FieldKind::Enum { .. }
505 | FieldKind::Int128
506 | FieldKind::IntBig { .. }
507 | FieldKind::List(_)
508 | FieldKind::Map { .. }
509 | FieldKind::Set(_)
510 | FieldKind::Structured { .. }
511 | FieldKind::Nat128
512 | FieldKind::NatBig { .. } => LeafCodec::StructuralFallback,
513 }
514}
515
516#[derive(Clone, Copy, Debug, Eq, PartialEq)]
523pub enum RelationStrength {
524 Strong,
525 Weak,
526}
527
528#[derive(Clone, Copy, Debug)]
538pub enum FieldKind {
539 Account,
541 Blob {
542 max_len: Option<u32>,
544 },
545 Bool,
546 Date,
547 Decimal {
548 scale: u32,
550 },
551 Duration,
552 Enum {
553 path: &'static str,
555 variants: &'static [EnumVariantModel],
557 },
558 Float32,
559 Float64,
560 Int8,
561 Int16,
562 Int32,
563 Int64,
564 Int128,
565 IntBig {
566 max_bytes: u32,
568 },
569 Principal,
570 Subaccount,
571 Text {
572 max_len: Option<u32>,
574 },
575 Timestamp,
576 Nat8,
577 Nat16,
578 Nat32,
579 Nat64,
580 Nat128,
581 NatBig {
582 max_bytes: u32,
584 },
585 Ulid,
586 Unit,
587
588 Relation {
591 target_path: &'static str,
593 target_entity_name: &'static str,
595 target_entity_tag: EntityTag,
597 target_store_path: &'static str,
599 key_kind: &'static Self,
600 strength: RelationStrength,
601 },
602
603 List(&'static Self),
605 Set(&'static Self),
606 Map {
610 key: &'static Self,
611 value: &'static Self,
612 },
613
614 Structured {
618 queryable: bool,
619 },
620}
621
622impl FieldKind {
623 #[must_use]
624 pub const fn value_kind(&self) -> RuntimeValueKind {
625 match self {
626 Self::Account
627 | Self::Blob { .. }
628 | Self::Bool
629 | Self::Date
630 | Self::Duration
631 | Self::Enum { .. }
632 | Self::Float32
633 | Self::Float64
634 | Self::Int8
635 | Self::Int16
636 | Self::Int32
637 | Self::Int64
638 | Self::Int128
639 | Self::IntBig { .. }
640 | Self::Principal
641 | Self::Subaccount
642 | Self::Text { .. }
643 | Self::Timestamp
644 | Self::Nat8
645 | Self::Nat16
646 | Self::Nat32
647 | Self::Nat64
648 | Self::Nat128
649 | Self::NatBig { .. }
650 | Self::Ulid
651 | Self::Unit
652 | Self::Decimal { .. }
653 | Self::Relation { .. } => RuntimeValueKind::Atomic,
654 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
655 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
656 Self::Structured { queryable } => RuntimeValueKind::Structured {
657 queryable: *queryable,
658 },
659 }
660 }
661
662 #[must_use]
670 pub const fn is_deterministic_collection_shape(&self) -> bool {
671 match self {
672 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
673
674 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
675
676 Self::Map { key, value } => {
677 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
678 }
679
680 _ => true,
681 }
682 }
683
684 #[must_use]
687 pub(crate) fn supports_group_probe(&self) -> bool {
688 match self {
689 Self::Enum { variants, .. } => variants.iter().all(|variant| {
690 variant
691 .payload_kind()
692 .is_none_or(Self::supports_group_probe)
693 }),
694 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
695 Self::List(_)
696 | Self::Set(_)
697 | Self::Map { .. }
698 | Self::Structured { .. }
699 | Self::Unit => false,
700 Self::Account
701 | Self::Blob { .. }
702 | Self::Bool
703 | Self::Date
704 | Self::Decimal { .. }
705 | Self::Duration
706 | Self::Float32
707 | Self::Float64
708 | Self::Int8
709 | Self::Int16
710 | Self::Int32
711 | Self::Int64
712 | Self::Int128
713 | Self::IntBig { .. }
714 | Self::Principal
715 | Self::Subaccount
716 | Self::Text { .. }
717 | Self::Timestamp
718 | Self::Nat8
719 | Self::Nat16
720 | Self::Nat32
721 | Self::Nat64
722 | Self::Nat128
723 | Self::NatBig { .. }
724 | Self::Ulid => true,
725 }
726 }
727
728 #[must_use]
734 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
735 match (self, value) {
736 (Self::Account, Value::Account(_))
737 | (Self::Blob { .. }, Value::Blob(_))
738 | (Self::Bool, Value::Bool(_))
739 | (Self::Date, Value::Date(_))
740 | (Self::Decimal { .. }, Value::Decimal(_))
741 | (Self::Duration, Value::Duration(_))
742 | (Self::Enum { .. }, Value::Enum(_))
743 | (Self::Float32, Value::Float32(_))
744 | (Self::Float64, Value::Float64(_))
745 | (Self::Int64, Value::Int64(_))
746 | (Self::Int128, Value::Int128(_))
747 | (Self::Nat64, Value::Nat64(_))
748 | (Self::Principal, Value::Principal(_))
749 | (Self::Subaccount, Value::Subaccount(_))
750 | (Self::Text { .. }, Value::Text(_))
751 | (Self::Timestamp, Value::Timestamp(_))
752 | (Self::Nat128, Value::Nat128(_))
753 | (Self::Ulid, Value::Ulid(_))
754 | (Self::Unit, Value::Unit)
755 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
756 (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
757 (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
758 (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
759 (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
760 (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
761 (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
762 (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
763 value.to_leb128().len() <= *max_bytes as usize
764 }
765 (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
766 value.to_leb128().len() <= *max_bytes as usize
767 }
768 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
769 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
770 items.iter().all(|item| inner.accepts_value(item))
771 }
772 (Self::Map { key, value }, Value::Map(entries)) => {
773 if Value::validate_map_entries(entries.as_slice()).is_err() {
774 return false;
775 }
776
777 entries.iter().all(|(entry_key, entry_value)| {
778 key.accepts_value(entry_key) && value.accepts_value(entry_value)
779 })
780 }
781 _ => false,
782 }
783 }
784}
785
786fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
792 match (kind, value) {
793 (FieldKind::Relation { key_kind, .. }, value) => {
794 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
795 }
796 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
797 .iter()
798 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
799 (
800 FieldKind::Map {
801 key,
802 value: value_kind,
803 },
804 Value::Map(entries),
805 ) => {
806 if Value::validate_map_entries(entries.as_slice()).is_err() {
807 return false;
808 }
809
810 entries.iter().all(|(entry_key, entry_value)| {
811 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
812 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
813 })
814 }
815 (FieldKind::Structured { .. }, _) => false,
816 _ => kind.accepts_value(value),
817 }
818}
819
820fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
824 match (kind, value) {
825 (FieldKind::Structured { .. }, _) => true,
826 (FieldKind::Relation { key_kind, .. }, value) => {
827 value_storage_kind_accepts_runtime_value(*key_kind, value)
828 }
829 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
830 .iter()
831 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
832 (
833 FieldKind::Map {
834 key,
835 value: value_kind,
836 },
837 Value::Map(entries),
838 ) => {
839 if Value::validate_map_entries(entries.as_slice()).is_err() {
840 return false;
841 }
842
843 entries.iter().all(|(entry_key, entry_value)| {
844 value_storage_kind_accepts_runtime_value(*key, entry_key)
845 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
846 })
847 }
848 _ => kind.accepts_value(value),
849 }
850}
851
852fn ensure_decimal_scale_matches(
855 kind: FieldKind,
856 value: &Value,
857) -> Result<(), FieldStorageValidationError> {
858 if matches!(value, Value::Null) {
859 return Ok(());
860 }
861
862 match (kind, value) {
863 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
864 if decimal.scale() != scale {
865 return Err(FieldStorageValidationError::DecimalScaleMismatch);
866 }
867
868 Ok(())
869 }
870 (FieldKind::Relation { key_kind, .. }, value) => {
871 ensure_decimal_scale_matches(*key_kind, value)
872 }
873 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
874 for item in items {
875 ensure_decimal_scale_matches(*inner, item)?;
876 }
877
878 Ok(())
879 }
880 (
881 FieldKind::Map {
882 key,
883 value: map_value,
884 },
885 Value::Map(entries),
886 ) => {
887 for (entry_key, entry_value) in entries {
888 ensure_decimal_scale_matches(*key, entry_key)?;
889 ensure_decimal_scale_matches(*map_value, entry_value)?;
890 }
891
892 Ok(())
893 }
894 _ => Ok(()),
895 }
896}
897
898pub(crate) fn normalize_decimal_scale_for_storage(
903 kind: FieldKind,
904 value: &Value,
905) -> Result<Cow<'_, Value>, FieldStorageValidationError> {
906 if matches!(value, Value::Null) {
907 return Ok(Cow::Borrowed(value));
908 }
909
910 match (kind, value) {
911 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
912 let normalized = decimal_with_storage_scale(*decimal, scale)
913 .ok_or(FieldStorageValidationError::DecimalScaleMismatch)?;
914
915 if normalized.scale() == decimal.scale() {
916 Ok(Cow::Borrowed(value))
917 } else {
918 Ok(Cow::Owned(Value::Decimal(normalized)))
919 }
920 }
921 (FieldKind::Relation { key_kind, .. }, value) => {
922 normalize_decimal_scale_for_storage(*key_kind, value)
923 }
924 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
925 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
926 items.map_or_else(
927 || Cow::Borrowed(value),
928 |items| Cow::Owned(Value::List(items)),
929 )
930 })
931 }
932 (
933 FieldKind::Map {
934 key,
935 value: map_value,
936 },
937 Value::Map(entries),
938 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
939 entries.map_or_else(
940 || Cow::Borrowed(value),
941 |entries| Cow::Owned(Value::Map(entries)),
942 )
943 }),
944 _ => Ok(Cow::Borrowed(value)),
945 }
946}
947
948fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
952 match decimal.scale().cmp(&scale) {
953 Ordering::Equal => Some(decimal),
954 Ordering::Less => decimal
955 .scale_to_integer(scale)
956 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
957 Ordering::Greater => Some(decimal.round_dp(scale)),
958 }
959}
960
961fn normalize_decimal_list_items(
964 kind: FieldKind,
965 items: &[Value],
966) -> Result<Option<Vec<Value>>, FieldStorageValidationError> {
967 let mut normalized_items = None;
968
969 for (index, item) in items.iter().enumerate() {
970 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
971 if let Cow::Owned(value) = normalized {
972 let items = normalized_items.get_or_insert_with(|| items.to_vec());
973 items[index] = value;
974 }
975 }
976
977 Ok(normalized_items)
978}
979
980fn normalize_decimal_map_entries(
983 key_kind: FieldKind,
984 value_kind: FieldKind,
985 entries: &[(Value, Value)],
986) -> Result<Option<Vec<(Value, Value)>>, FieldStorageValidationError> {
987 let mut normalized_entries = None;
988
989 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
990 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
991 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
992
993 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
994 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
995 if let Cow::Owned(value) = normalized_key {
996 entries[index].0 = value;
997 }
998 if let Cow::Owned(value) = normalized_value {
999 entries[index].1 = value;
1000 }
1001 }
1002 }
1003
1004 Ok(normalized_entries)
1005}
1006
1007fn ensure_scalar_max_len_matches(
1010 kind: FieldKind,
1011 value: &Value,
1012) -> Result<(), FieldStorageValidationError> {
1013 if matches!(value, Value::Null) {
1014 return Ok(());
1015 }
1016
1017 match (kind, value) {
1018 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
1019 let len = text.chars().count();
1020 if len > max as usize {
1021 return Err(FieldStorageValidationError::ScalarMaxLen);
1022 }
1023
1024 Ok(())
1025 }
1026 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
1027 let len = bytes.len();
1028 if len > max as usize {
1029 return Err(FieldStorageValidationError::ScalarMaxLen);
1030 }
1031
1032 Ok(())
1033 }
1034 (FieldKind::Relation { key_kind, .. }, value) => {
1035 ensure_scalar_max_len_matches(*key_kind, value)
1036 }
1037 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1038 for item in items {
1039 ensure_scalar_max_len_matches(*inner, item)?;
1040 }
1041
1042 Ok(())
1043 }
1044 (
1045 FieldKind::Map {
1046 key,
1047 value: map_value,
1048 },
1049 Value::Map(entries),
1050 ) => {
1051 for (entry_key, entry_value) in entries {
1052 ensure_scalar_max_len_matches(*key, entry_key)?;
1053 ensure_scalar_max_len_matches(*map_value, entry_value)?;
1054 }
1055
1056 Ok(())
1057 }
1058 _ => Ok(()),
1059 }
1060}
1061
1062fn ensure_value_is_deterministic_for_storage(
1065 kind: FieldKind,
1066 value: &Value,
1067) -> Result<(), FieldStorageValidationError> {
1068 match (kind, value) {
1069 (FieldKind::Set(_), Value::List(items)) => {
1070 for pair in items.windows(2) {
1071 let [left, right] = pair else {
1072 continue;
1073 };
1074 if Value::canonical_cmp(left, right) != Ordering::Less {
1075 return Err(FieldStorageValidationError::SetCanonicalOrder);
1076 }
1077 }
1078
1079 Ok(())
1080 }
1081 (FieldKind::Map { .. }, Value::Map(entries)) => {
1082 Value::validate_map_entries(entries.as_slice())
1083 .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
1084
1085 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1086 return Err(FieldStorageValidationError::MapCanonicalOrder);
1087 }
1088
1089 Ok(())
1090 }
1091 _ => Ok(()),
1092 }
1093}
1094
1095#[cfg(test)]
1100mod tests {
1101 use crate::{
1102 model::field::{FieldKind, FieldModel},
1103 value::Value,
1104 };
1105
1106 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1107 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1108
1109 #[test]
1110 fn text_max_len_accepts_unbounded_text() {
1111 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1112
1113 assert!(
1114 field
1115 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1116 .is_ok()
1117 );
1118 }
1119
1120 #[test]
1121 fn text_max_len_counts_unicode_scalars_not_bytes() {
1122 let field = FieldModel::generated("name", BOUNDED_TEXT);
1123
1124 assert!(
1125 field
1126 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1127 .is_ok()
1128 );
1129 assert!(
1130 field
1131 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1132 .is_err()
1133 );
1134 }
1135
1136 #[test]
1137 fn text_max_len_recurses_through_collections() {
1138 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1139 static TEXT_MAP: FieldKind = FieldKind::Map {
1140 key: &BOUNDED_TEXT,
1141 value: &BOUNDED_TEXT,
1142 };
1143
1144 let list_field = FieldModel::generated("names", TEXT_LIST);
1145 let map_field = FieldModel::generated("labels", TEXT_MAP);
1146
1147 assert!(
1148 list_field
1149 .validate_runtime_value_for_storage(&Value::List(vec![
1150 Value::Text("Ada".into()),
1151 Value::Text("Bob".into()),
1152 ]))
1153 .is_ok()
1154 );
1155 assert!(
1156 list_field
1157 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1158 .is_err()
1159 );
1160 assert!(
1161 map_field
1162 .validate_runtime_value_for_storage(&Value::Map(vec![(
1163 Value::Text("key".into()),
1164 Value::Text("val".into()),
1165 )]))
1166 .is_ok()
1167 );
1168 assert!(
1169 map_field
1170 .validate_runtime_value_for_storage(&Value::Map(vec![(
1171 Value::Text("long".into()),
1172 Value::Text("val".into()),
1173 )]))
1174 .is_err()
1175 );
1176 }
1177
1178 #[test]
1179 fn blob_max_len_counts_bytes() {
1180 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1181
1182 assert!(
1183 field
1184 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1185 .is_ok()
1186 );
1187 assert!(
1188 field
1189 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1190 .is_err()
1191 );
1192 }
1193
1194 #[test]
1195 fn blob_max_len_recurses_through_collections() {
1196 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1197
1198 let field = FieldModel::generated("payloads", BLOB_LIST);
1199
1200 assert!(
1201 field
1202 .validate_runtime_value_for_storage(&Value::List(vec![
1203 Value::Blob(vec![1, 2, 3]),
1204 Value::Blob(vec![4, 5]),
1205 ]))
1206 .is_ok()
1207 );
1208 assert!(
1209 field
1210 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1211 1, 2, 3, 4
1212 ])]))
1213 .is_err()
1214 );
1215 }
1216}