1#[cfg(test)]
7use crate::value::Value;
8use crate::{types::EntityTag, value::RuntimeValueKind};
9#[cfg(test)]
10use std::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 CatalogValue,
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),
85 Structural,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum CompositeCodec {
100 StructuralV1,
102}
103
104#[derive(Clone, Copy, Debug)]
113pub struct CompositeFieldModel {
114 pub(crate) name: &'static str,
116 pub(crate) kind: FieldKind,
118 pub(crate) nullable: bool,
120}
121
122impl CompositeFieldModel {
123 #[must_use]
125 #[doc(hidden)]
126 pub const fn generated(name: &'static str, kind: FieldKind, nullable: bool) -> Self {
127 Self {
128 name,
129 kind,
130 nullable,
131 }
132 }
133
134 #[must_use]
136 pub const fn name(&self) -> &'static str {
137 self.name
138 }
139
140 #[must_use]
142 pub const fn kind(&self) -> FieldKind {
143 self.kind
144 }
145
146 #[must_use]
148 pub const fn nullable(&self) -> bool {
149 self.nullable
150 }
151}
152
153#[derive(Clone, Copy, Debug)]
161pub struct CompositeElementModel {
162 pub(crate) kind: FieldKind,
164 pub(crate) nullable: bool,
166}
167
168impl CompositeElementModel {
169 #[must_use]
171 #[doc(hidden)]
172 pub const fn generated(kind: FieldKind, nullable: bool) -> Self {
173 Self { kind, nullable }
174 }
175
176 #[must_use]
178 pub const fn kind(&self) -> FieldKind {
179 self.kind
180 }
181
182 #[must_use]
184 pub const fn nullable(&self) -> bool {
185 self.nullable
186 }
187}
188
189#[derive(Clone, Copy, Debug)]
198pub enum CompositeShapeModel {
199 Record(&'static [CompositeFieldModel]),
201 Tuple(&'static [CompositeElementModel]),
203 Newtype(CompositeElementModel),
205}
206
207#[derive(Clone, Copy, Debug)]
217pub struct EnumVariantModel {
218 pub(crate) ident: &'static str,
220 payload_kind: Option<EnumPayloadKindModel>,
222 pub(crate) payload_storage_decode: FieldStorageDecode,
224}
225
226impl EnumVariantModel {
227 #[must_use]
229 pub const fn new(
230 ident: &'static str,
231 payload_kind: Option<&'static FieldKind>,
232 payload_storage_decode: FieldStorageDecode,
233 ) -> Self {
234 Self {
235 ident,
236 payload_kind: match payload_kind {
237 Some(kind) => Some(EnumPayloadKindModel::Static(kind)),
238 None => None,
239 },
240 payload_storage_decode,
241 }
242 }
243
244 #[must_use]
247 #[doc(hidden)]
248 pub const fn generated_with_payload_kind_resolver(
249 ident: &'static str,
250 payload_kind: Option<fn() -> FieldKind>,
251 payload_storage_decode: FieldStorageDecode,
252 ) -> Self {
253 Self {
254 ident,
255 payload_kind: match payload_kind {
256 Some(resolve) => Some(EnumPayloadKindModel::Generated(resolve)),
257 None => None,
258 },
259 payload_storage_decode,
260 }
261 }
262
263 #[must_use]
265 pub const fn ident(&self) -> &'static str {
266 self.ident
267 }
268
269 #[must_use]
271 pub fn payload_kind(&self) -> Option<FieldKind> {
272 self.payload_kind.map(EnumPayloadKindModel::resolve)
273 }
274
275 #[must_use]
277 pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
278 self.payload_storage_decode
279 }
280}
281
282#[derive(Clone, Copy, Debug)]
283enum EnumPayloadKindModel {
284 Static(&'static FieldKind),
285 Generated(fn() -> FieldKind),
286}
287
288impl EnumPayloadKindModel {
289 fn resolve(self) -> FieldKind {
290 match self {
291 Self::Static(kind) => *kind,
292 Self::Generated(resolve) => resolve(),
293 }
294 }
295}
296
297#[derive(Debug)]
307pub struct FieldModel {
308 pub(crate) name: &'static str,
310 pub(crate) kind: FieldKind,
312 pub(crate) nested_fields: &'static [Self],
314 pub(crate) nullable: bool,
316 pub(crate) storage_decode: FieldStorageDecode,
318 pub(crate) leaf_codec: LeafCodec,
320 pub(crate) insert_generation: Option<FieldInsertGeneration>,
322 pub(crate) write_management: Option<FieldWriteManagement>,
324 pub(crate) database_default: FieldDatabaseDefault,
326}
327
328#[derive(Clone, Copy, Debug, Eq, PartialEq)]
337pub enum FieldDatabaseDefault {
338 None,
340 EncodedSlotPayload(&'static [u8]),
346 AuthoredEnumUnit {
351 enum_path: &'static str,
353 variant: &'static str,
355 },
356}
357
358#[derive(Clone, Copy, Debug, Eq, PartialEq)]
368pub enum FieldInsertGeneration {
369 Ulid,
371 Timestamp,
373}
374
375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
385pub enum FieldWriteManagement {
386 CreatedAt,
388 UpdatedAt,
390}
391
392impl FieldModel {
393 #[must_use]
399 #[doc(hidden)]
400 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
401 Self::generated_with_storage_decode_and_nullability(
402 name,
403 kind,
404 FieldStorageDecode::ByKind,
405 false,
406 )
407 }
408
409 #[must_use]
411 #[doc(hidden)]
412 pub const fn generated_with_storage_decode(
413 name: &'static str,
414 kind: FieldKind,
415 storage_decode: FieldStorageDecode,
416 ) -> Self {
417 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
418 }
419
420 #[must_use]
422 #[doc(hidden)]
423 pub const fn generated_with_storage_decode_and_nullability(
424 name: &'static str,
425 kind: FieldKind,
426 storage_decode: FieldStorageDecode,
427 nullable: bool,
428 ) -> Self {
429 Self::generated_with_storage_decode_nullability_and_write_policies(
430 name,
431 kind,
432 storage_decode,
433 nullable,
434 None,
435 None,
436 )
437 }
438
439 #[must_use]
442 #[doc(hidden)]
443 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
444 name: &'static str,
445 kind: FieldKind,
446 storage_decode: FieldStorageDecode,
447 nullable: bool,
448 insert_generation: Option<FieldInsertGeneration>,
449 ) -> Self {
450 Self::generated_with_storage_decode_nullability_and_write_policies(
451 name,
452 kind,
453 storage_decode,
454 nullable,
455 insert_generation,
456 None,
457 )
458 }
459
460 #[must_use]
463 #[doc(hidden)]
464 pub const fn generated_with_storage_decode_nullability_and_write_policies(
465 name: &'static str,
466 kind: FieldKind,
467 storage_decode: FieldStorageDecode,
468 nullable: bool,
469 insert_generation: Option<FieldInsertGeneration>,
470 write_management: Option<FieldWriteManagement>,
471 ) -> Self {
472 Self {
473 name,
474 kind,
475 nested_fields: &[],
476 nullable,
477 storage_decode,
478 leaf_codec: leaf_codec_for(kind, storage_decode),
479 insert_generation,
480 write_management,
481 database_default: FieldDatabaseDefault::None,
482 }
483 }
484
485 #[must_use]
487 #[doc(hidden)]
488 pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
489 name: &'static str,
490 kind: FieldKind,
491 storage_decode: FieldStorageDecode,
492 nullable: bool,
493 insert_generation: Option<FieldInsertGeneration>,
494 write_management: Option<FieldWriteManagement>,
495 nested_fields: &'static [Self],
496 ) -> Self {
497 Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
498 name,
499 kind,
500 storage_decode,
501 nullable,
502 insert_generation,
503 write_management,
504 FieldDatabaseDefault::None,
505 nested_fields,
506 )
507 }
508
509 #[must_use]
512 #[doc(hidden)]
513 #[expect(
514 clippy::too_many_arguments,
515 reason = "generated schema metadata keeps every field contract explicit"
516 )]
517 pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
518 name: &'static str,
519 kind: FieldKind,
520 storage_decode: FieldStorageDecode,
521 nullable: bool,
522 insert_generation: Option<FieldInsertGeneration>,
523 write_management: Option<FieldWriteManagement>,
524 database_default: FieldDatabaseDefault,
525 nested_fields: &'static [Self],
526 ) -> Self {
527 Self {
528 name,
529 kind,
530 nested_fields,
531 nullable,
532 storage_decode,
533 leaf_codec: leaf_codec_for(kind, storage_decode),
534 insert_generation,
535 write_management,
536 database_default,
537 }
538 }
539
540 #[must_use]
542 pub const fn name(&self) -> &'static str {
543 self.name
544 }
545
546 #[must_use]
548 pub const fn kind(&self) -> FieldKind {
549 self.kind
550 }
551
552 #[must_use]
554 pub const fn nested_fields(&self) -> &'static [Self] {
555 self.nested_fields
556 }
557
558 #[must_use]
560 pub const fn nullable(&self) -> bool {
561 self.nullable
562 }
563
564 #[must_use]
566 pub const fn storage_decode(&self) -> FieldStorageDecode {
567 self.storage_decode
568 }
569
570 #[must_use]
572 pub const fn leaf_codec(&self) -> LeafCodec {
573 self.leaf_codec
574 }
575
576 #[must_use]
578 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
579 self.insert_generation
580 }
581
582 #[must_use]
584 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
585 self.write_management
586 }
587
588 #[must_use]
590 pub const fn database_default(&self) -> FieldDatabaseDefault {
591 self.database_default
592 }
593
594 #[cfg(test)]
601 pub(crate) fn validate_runtime_value_for_storage(
602 &self,
603 value: &Value,
604 ) -> Result<(), FieldStorageValidationError> {
605 if matches!(value, Value::Null) {
606 if self.nullable() {
607 return Ok(());
608 }
609
610 return Err(FieldStorageValidationError::RequiredFieldNull);
611 }
612
613 let accepts = match self.storage_decode() {
614 FieldStorageDecode::CatalogValue => {
615 value_storage_kind_accepts_runtime_value(self.kind(), value)
616 }
617 FieldStorageDecode::ByKind => {
618 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
619 }
620 };
621 if !accepts {
622 return Err(FieldStorageValidationError::StorageContract);
623 }
624
625 ensure_decimal_scale_matches(self.kind(), value)?;
626 ensure_scalar_max_len_matches(self.kind(), value)?;
627 ensure_value_is_deterministic_for_storage(self.kind(), value)
628 }
629}
630
631pub(crate) const fn leaf_codec_for(
635 kind: FieldKind,
636 storage_decode: FieldStorageDecode,
637) -> LeafCodec {
638 if matches!(storage_decode, FieldStorageDecode::CatalogValue) {
639 return LeafCodec::Structural;
640 }
641
642 match kind {
643 FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
644 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
645 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
646 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
647 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
648 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
649 FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
650 LeafCodec::Scalar(ScalarCodec::Int64)
651 }
652 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
653 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
654 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
655 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
656 FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
657 LeafCodec::Scalar(ScalarCodec::Nat64)
658 }
659 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
660 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
661 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
662 FieldKind::Account
663 | FieldKind::Decimal { .. }
664 | FieldKind::Enum { .. }
665 | FieldKind::Int128
666 | FieldKind::IntBig { .. }
667 | FieldKind::List(_)
668 | FieldKind::Map { .. }
669 | FieldKind::Set(_)
670 | FieldKind::Composite { .. }
671 | FieldKind::Nat128
672 | FieldKind::NatBig { .. } => LeafCodec::Structural,
673 }
674}
675
676#[derive(Clone, Copy, Debug)]
687pub enum FieldKind {
688 Account,
690 Blob {
691 max_len: Option<u32>,
693 },
694 Bool,
695 Date,
696 Decimal {
697 scale: u32,
699 },
700 Duration,
701 Enum {
702 path: &'static str,
704 variants: &'static [EnumVariantModel],
706 },
707 Float32,
708 Float64,
709 Int8,
710 Int16,
711 Int32,
712 Int64,
713 Int128,
714 IntBig {
715 max_bytes: u32,
717 },
718 Principal,
719 Subaccount,
720 Text {
721 max_len: Option<u32>,
723 },
724 Timestamp,
725 Nat8,
726 Nat16,
727 Nat32,
728 Nat64,
729 Nat128,
730 NatBig {
731 max_bytes: u32,
733 },
734 Ulid,
735 Unit,
736
737 Relation {
739 target_path: &'static str,
741 target_entity_name: &'static str,
743 target_entity_tag: EntityTag,
745 target_store_path: &'static str,
747 key_kind: &'static Self,
748 },
749
750 List(&'static Self),
752 Set(&'static Self),
753 Map {
757 key: &'static Self,
758 value: &'static Self,
759 },
760
761 Composite {
763 path: &'static str,
765 codec: CompositeCodec,
767 shape: &'static CompositeShapeModel,
769 },
770}
771
772#[cfg(test)]
773static EMPTY_TEST_COMPOSITE_FIELDS: [CompositeFieldModel; 0] = [];
774#[cfg(test)]
775static EMPTY_TEST_COMPOSITE_SHAPE: CompositeShapeModel =
776 CompositeShapeModel::Record(&EMPTY_TEST_COMPOSITE_FIELDS);
777
778impl FieldKind {
779 #[cfg(test)]
781 #[must_use]
782 pub(crate) const fn empty_test_composite(path: &'static str) -> Self {
783 Self::Composite {
784 path,
785 codec: CompositeCodec::StructuralV1,
786 shape: &EMPTY_TEST_COMPOSITE_SHAPE,
787 }
788 }
789
790 #[must_use]
791 pub const fn value_kind(&self) -> RuntimeValueKind {
792 match self {
793 Self::Account
794 | Self::Blob { .. }
795 | Self::Bool
796 | Self::Date
797 | Self::Duration
798 | Self::Enum { .. }
799 | Self::Float32
800 | Self::Float64
801 | Self::Int8
802 | Self::Int16
803 | Self::Int32
804 | Self::Int64
805 | Self::Int128
806 | Self::IntBig { .. }
807 | Self::Principal
808 | Self::Subaccount
809 | Self::Text { .. }
810 | Self::Timestamp
811 | Self::Nat8
812 | Self::Nat16
813 | Self::Nat32
814 | Self::Nat64
815 | Self::Nat128
816 | Self::NatBig { .. }
817 | Self::Ulid
818 | Self::Unit
819 | Self::Decimal { .. }
820 | Self::Relation { .. } => RuntimeValueKind::Atomic,
821 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
822 Self::Map { .. } | Self::Composite { .. } => {
823 RuntimeValueKind::Structured { queryable: false }
824 }
825 }
826 }
827
828 #[must_use]
836 pub const fn is_deterministic_collection_shape(&self) -> bool {
837 match self {
838 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
839
840 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
841
842 Self::Map { key, value } => {
843 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
844 }
845
846 _ => true,
847 }
848 }
849
850 #[cfg(test)]
853 #[must_use]
854 pub(crate) fn supports_group_probe(&self) -> bool {
855 match self {
856 Self::Enum { variants, .. } => variants.iter().all(|variant| {
857 variant
858 .payload_kind()
859 .is_none_or(|kind| Self::supports_group_probe(&kind))
860 }),
861 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
862 Self::Decimal { .. } => true,
863 _ => super::field_kind_semantics::field_kind_has_identity_group_canonical_form(*self),
864 }
865 }
866
867 #[cfg(test)]
869 #[must_use]
870 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
871 match (self, value) {
872 (Self::Account, Value::Account(_))
873 | (Self::Blob { .. }, Value::Blob(_))
874 | (Self::Bool, Value::Bool(_))
875 | (Self::Date, Value::Date(_))
876 | (Self::Decimal { .. }, Value::Decimal(_))
877 | (Self::Duration, Value::Duration(_))
878 | (Self::Enum { .. }, Value::Enum(_))
879 | (Self::Float32, Value::Float32(_))
880 | (Self::Float64, Value::Float64(_))
881 | (Self::Int64, Value::Int64(_))
882 | (Self::Int128, Value::Int128(_))
883 | (Self::Nat64, Value::Nat64(_))
884 | (Self::Principal, Value::Principal(_))
885 | (Self::Subaccount, Value::Subaccount(_))
886 | (Self::Text { .. }, Value::Text(_))
887 | (Self::Timestamp, Value::Timestamp(_))
888 | (Self::Nat128, Value::Nat128(_))
889 | (Self::Ulid, Value::Ulid(_))
890 | (Self::Unit, Value::Unit) => true,
891 (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
892 (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
893 (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
894 (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
895 (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
896 (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
897 (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
898 value.to_leb128().len() <= *max_bytes as usize
899 }
900 (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
901 value.to_leb128().len() <= *max_bytes as usize
902 }
903 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
904 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
905 items.iter().all(|item| inner.accepts_value(item))
906 }
907 (Self::Map { key, value }, Value::Map(entries)) => {
908 Value::validate_map_entries(entries.as_slice()).is_ok()
909 && entries.iter().all(|(entry_key, entry_value)| {
910 key.accepts_value(entry_key) && value.accepts_value(entry_value)
911 })
912 }
913 (Self::Composite { shape, .. }, value) => shape.accepts_value(value),
914 _ => false,
915 }
916 }
917}
918
919#[cfg(test)]
920impl CompositeShapeModel {
921 fn accepts_value(self, value: &Value) -> bool {
922 match (self, value) {
923 (Self::Record(fields), Value::Map(entries)) => {
924 entries.len() == fields.len()
925 && entries.iter().zip(fields).all(|((key, value), field)| {
926 matches!(key, Value::Text(name) if name == field.name())
927 && accepts_nullable_value(field.kind(), field.nullable(), value)
928 })
929 }
930 (Self::Tuple(elements), Value::List(values)) => {
931 values.len() == elements.len()
932 && values.iter().zip(elements).all(|(value, element)| {
933 accepts_nullable_value(element.kind(), element.nullable(), value)
934 })
935 }
936 (Self::Newtype(inner), value) => {
937 accepts_nullable_value(inner.kind(), inner.nullable(), value)
938 }
939 _ => false,
940 }
941 }
942}
943
944#[cfg(test)]
945fn accepts_nullable_value(kind: FieldKind, nullable: bool, value: &Value) -> bool {
946 matches!(value, Value::Null) && nullable || kind.accepts_value(value)
947}
948
949#[cfg(test)]
953fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
954 match (kind, value) {
955 (FieldKind::Relation { key_kind, .. }, value) => {
956 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
957 }
958 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
959 .iter()
960 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
961 (
962 FieldKind::Map {
963 key,
964 value: value_kind,
965 },
966 Value::Map(entries),
967 ) => {
968 if Value::validate_map_entries(entries.as_slice()).is_err() {
969 return false;
970 }
971
972 entries.iter().all(|(entry_key, entry_value)| {
973 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
974 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
975 })
976 }
977 _ => kind.accepts_value(value),
978 }
979}
980
981#[cfg(test)]
984fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
985 match (kind, value) {
986 (FieldKind::Relation { key_kind, .. }, value) => {
987 value_storage_kind_accepts_runtime_value(*key_kind, value)
988 }
989 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
990 .iter()
991 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
992 (
993 FieldKind::Map {
994 key,
995 value: value_kind,
996 },
997 Value::Map(entries),
998 ) => {
999 if Value::validate_map_entries(entries.as_slice()).is_err() {
1000 return false;
1001 }
1002
1003 entries.iter().all(|(entry_key, entry_value)| {
1004 value_storage_kind_accepts_runtime_value(*key, entry_key)
1005 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
1006 })
1007 }
1008 _ => kind.accepts_value(value),
1009 }
1010}
1011
1012#[cfg(test)]
1015fn ensure_decimal_scale_matches(
1016 kind: FieldKind,
1017 value: &Value,
1018) -> Result<(), FieldStorageValidationError> {
1019 if matches!(value, Value::Null) {
1020 return Ok(());
1021 }
1022
1023 match (kind, value) {
1024 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
1025 if decimal.scale() != scale {
1026 return Err(FieldStorageValidationError::DecimalScaleMismatch);
1027 }
1028
1029 Ok(())
1030 }
1031 (FieldKind::Relation { key_kind, .. }, value) => {
1032 ensure_decimal_scale_matches(*key_kind, value)
1033 }
1034 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1035 for item in items {
1036 ensure_decimal_scale_matches(*inner, item)?;
1037 }
1038
1039 Ok(())
1040 }
1041 (
1042 FieldKind::Map {
1043 key,
1044 value: map_value,
1045 },
1046 Value::Map(entries),
1047 ) => {
1048 for (entry_key, entry_value) in entries {
1049 ensure_decimal_scale_matches(*key, entry_key)?;
1050 ensure_decimal_scale_matches(*map_value, entry_value)?;
1051 }
1052
1053 Ok(())
1054 }
1055 _ => Ok(()),
1056 }
1057}
1058
1059#[cfg(test)]
1062fn ensure_scalar_max_len_matches(
1063 kind: FieldKind,
1064 value: &Value,
1065) -> Result<(), FieldStorageValidationError> {
1066 if matches!(value, Value::Null) {
1067 return Ok(());
1068 }
1069
1070 match (kind, value) {
1071 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
1072 let len = text.chars().count();
1073 if len > max as usize {
1074 return Err(FieldStorageValidationError::ScalarMaxLen);
1075 }
1076
1077 Ok(())
1078 }
1079 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
1080 let len = bytes.len();
1081 if len > max as usize {
1082 return Err(FieldStorageValidationError::ScalarMaxLen);
1083 }
1084
1085 Ok(())
1086 }
1087 (FieldKind::Relation { key_kind, .. }, value) => {
1088 ensure_scalar_max_len_matches(*key_kind, value)
1089 }
1090 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1091 for item in items {
1092 ensure_scalar_max_len_matches(*inner, item)?;
1093 }
1094
1095 Ok(())
1096 }
1097 (
1098 FieldKind::Map {
1099 key,
1100 value: map_value,
1101 },
1102 Value::Map(entries),
1103 ) => {
1104 for (entry_key, entry_value) in entries {
1105 ensure_scalar_max_len_matches(*key, entry_key)?;
1106 ensure_scalar_max_len_matches(*map_value, entry_value)?;
1107 }
1108
1109 Ok(())
1110 }
1111 _ => Ok(()),
1112 }
1113}
1114
1115#[cfg(test)]
1118fn ensure_value_is_deterministic_for_storage(
1119 kind: FieldKind,
1120 value: &Value,
1121) -> Result<(), FieldStorageValidationError> {
1122 match (kind, value) {
1123 (FieldKind::Set(_), Value::List(items)) => {
1124 for pair in items.windows(2) {
1125 let [left, right] = pair else {
1126 continue;
1127 };
1128 if Value::canonical_cmp(left, right) != Ordering::Less {
1129 return Err(FieldStorageValidationError::SetCanonicalOrder);
1130 }
1131 }
1132
1133 Ok(())
1134 }
1135 (FieldKind::Map { .. }, Value::Map(entries)) => {
1136 Value::validate_map_entries(entries.as_slice())
1137 .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
1138
1139 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1140 return Err(FieldStorageValidationError::MapCanonicalOrder);
1141 }
1142
1143 Ok(())
1144 }
1145 _ => Ok(()),
1146 }
1147}
1148
1149#[cfg(test)]
1154mod tests {
1155 use crate::{
1156 model::field::{FieldKind, FieldModel},
1157 value::Value,
1158 };
1159
1160 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1161 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1162
1163 #[test]
1164 fn text_max_len_accepts_unbounded_text() {
1165 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1166
1167 assert!(
1168 field
1169 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1170 .is_ok()
1171 );
1172 }
1173
1174 #[test]
1175 fn text_max_len_counts_unicode_scalars_not_bytes() {
1176 let field = FieldModel::generated("name", BOUNDED_TEXT);
1177
1178 assert!(
1179 field
1180 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1181 .is_ok()
1182 );
1183 assert!(
1184 field
1185 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1186 .is_err()
1187 );
1188 }
1189
1190 #[test]
1191 fn text_max_len_recurses_through_collections() {
1192 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1193 static TEXT_MAP: FieldKind = FieldKind::Map {
1194 key: &BOUNDED_TEXT,
1195 value: &BOUNDED_TEXT,
1196 };
1197
1198 let list_field = FieldModel::generated("names", TEXT_LIST);
1199 let map_field = FieldModel::generated("labels", TEXT_MAP);
1200
1201 assert!(
1202 list_field
1203 .validate_runtime_value_for_storage(&Value::List(vec![
1204 Value::Text("Ada".into()),
1205 Value::Text("Bob".into()),
1206 ]))
1207 .is_ok()
1208 );
1209 assert!(
1210 list_field
1211 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1212 .is_err()
1213 );
1214 assert!(
1215 map_field
1216 .validate_runtime_value_for_storage(&Value::Map(vec![(
1217 Value::Text("key".into()),
1218 Value::Text("val".into()),
1219 )]))
1220 .is_ok()
1221 );
1222 assert!(
1223 map_field
1224 .validate_runtime_value_for_storage(&Value::Map(vec![(
1225 Value::Text("long".into()),
1226 Value::Text("val".into()),
1227 )]))
1228 .is_err()
1229 );
1230 }
1231
1232 #[test]
1233 fn blob_max_len_counts_bytes() {
1234 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1235
1236 assert!(
1237 field
1238 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1239 .is_ok()
1240 );
1241 assert!(
1242 field
1243 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1244 .is_err()
1245 );
1246 }
1247
1248 #[test]
1249 fn blob_max_len_recurses_through_collections() {
1250 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1251
1252 let field = FieldModel::generated("payloads", BLOB_LIST);
1253
1254 assert!(
1255 field
1256 .validate_runtime_value_for_storage(&Value::List(vec![
1257 Value::Blob(vec![1, 2, 3]),
1258 Value::Blob(vec![4, 5]),
1259 ]))
1260 .is_ok()
1261 );
1262 assert!(
1263 field
1264 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1265 1, 2, 3, 4
1266 ])]))
1267 .is_err()
1268 );
1269 }
1270}