1use crate::{traits::RuntimeValueKind, types::EntityTag, value::Value};
7use std::cmp::Ordering;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum FieldStorageDecode {
21 ByKind,
23 Value,
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum ScalarCodec {
38 Blob,
39 Bool,
40 Date,
41 Duration,
42 Float32,
43 Float64,
44 Int64,
45 Principal,
46 Subaccount,
47 Text,
48 Timestamp,
49 Uint64,
50 Ulid,
51 Unit,
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum LeafCodec {
65 Scalar(ScalarCodec),
66 StructuralFallback,
67}
68
69#[derive(Clone, Copy, Debug)]
79pub struct EnumVariantModel {
80 pub(crate) ident: &'static str,
82 pub(crate) payload_kind: Option<&'static FieldKind>,
84 pub(crate) payload_storage_decode: FieldStorageDecode,
86}
87
88impl EnumVariantModel {
89 #[must_use]
91 pub const fn new(
92 ident: &'static str,
93 payload_kind: Option<&'static FieldKind>,
94 payload_storage_decode: FieldStorageDecode,
95 ) -> Self {
96 Self {
97 ident,
98 payload_kind,
99 payload_storage_decode,
100 }
101 }
102
103 #[must_use]
105 pub const fn ident(&self) -> &'static str {
106 self.ident
107 }
108
109 #[must_use]
111 pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
112 self.payload_kind
113 }
114
115 #[must_use]
117 pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
118 self.payload_storage_decode
119 }
120}
121
122#[derive(Debug)]
132pub struct FieldModel {
133 pub(crate) name: &'static str,
135 pub(crate) kind: FieldKind,
137 pub(crate) nullable: bool,
139 pub(crate) storage_decode: FieldStorageDecode,
141 pub(crate) leaf_codec: LeafCodec,
143 pub(crate) insert_generation: Option<FieldInsertGeneration>,
145 pub(crate) write_management: Option<FieldWriteManagement>,
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159pub enum FieldInsertGeneration {
160 Ulid,
162 Timestamp,
164}
165
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub enum FieldWriteManagement {
177 CreatedAt,
179 UpdatedAt,
181}
182
183impl FieldModel {
184 #[must_use]
190 #[doc(hidden)]
191 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
192 Self::generated_with_storage_decode_and_nullability(
193 name,
194 kind,
195 FieldStorageDecode::ByKind,
196 false,
197 )
198 }
199
200 #[must_use]
202 #[doc(hidden)]
203 pub const fn generated_with_storage_decode(
204 name: &'static str,
205 kind: FieldKind,
206 storage_decode: FieldStorageDecode,
207 ) -> Self {
208 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
209 }
210
211 #[must_use]
213 #[doc(hidden)]
214 pub const fn generated_with_storage_decode_and_nullability(
215 name: &'static str,
216 kind: FieldKind,
217 storage_decode: FieldStorageDecode,
218 nullable: bool,
219 ) -> Self {
220 Self::generated_with_storage_decode_nullability_and_write_policies(
221 name,
222 kind,
223 storage_decode,
224 nullable,
225 None,
226 None,
227 )
228 }
229
230 #[must_use]
233 #[doc(hidden)]
234 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
235 name: &'static str,
236 kind: FieldKind,
237 storage_decode: FieldStorageDecode,
238 nullable: bool,
239 insert_generation: Option<FieldInsertGeneration>,
240 ) -> Self {
241 Self::generated_with_storage_decode_nullability_and_write_policies(
242 name,
243 kind,
244 storage_decode,
245 nullable,
246 insert_generation,
247 None,
248 )
249 }
250
251 #[must_use]
254 #[doc(hidden)]
255 pub const fn generated_with_storage_decode_nullability_and_write_policies(
256 name: &'static str,
257 kind: FieldKind,
258 storage_decode: FieldStorageDecode,
259 nullable: bool,
260 insert_generation: Option<FieldInsertGeneration>,
261 write_management: Option<FieldWriteManagement>,
262 ) -> Self {
263 Self {
264 name,
265 kind,
266 nullable,
267 storage_decode,
268 leaf_codec: leaf_codec_for(kind, storage_decode),
269 insert_generation,
270 write_management,
271 }
272 }
273
274 #[must_use]
276 pub const fn name(&self) -> &'static str {
277 self.name
278 }
279
280 #[must_use]
282 pub const fn kind(&self) -> FieldKind {
283 self.kind
284 }
285
286 #[must_use]
288 pub const fn nullable(&self) -> bool {
289 self.nullable
290 }
291
292 #[must_use]
294 pub const fn storage_decode(&self) -> FieldStorageDecode {
295 self.storage_decode
296 }
297
298 #[must_use]
300 pub const fn leaf_codec(&self) -> LeafCodec {
301 self.leaf_codec
302 }
303
304 #[must_use]
306 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
307 self.insert_generation
308 }
309
310 #[must_use]
312 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
313 self.write_management
314 }
315
316 pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
324 if matches!(value, Value::Null) {
325 if self.nullable() {
326 return Ok(());
327 }
328
329 return Err("required field cannot store null".into());
330 }
331
332 let accepts = match self.storage_decode() {
333 FieldStorageDecode::Value => {
334 value_storage_kind_accepts_runtime_value(self.kind(), value)
335 }
336 FieldStorageDecode::ByKind => {
337 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
338 }
339 };
340 if !accepts {
341 return Err(format!(
342 "field kind {:?} does not accept runtime value {value:?}",
343 self.kind()
344 ));
345 }
346
347 ensure_decimal_scale_matches(self.kind(), value)?;
348 ensure_value_is_deterministic_for_storage(self.kind(), value)
349 }
350}
351
352const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
356 if matches!(storage_decode, FieldStorageDecode::Value) {
357 return LeafCodec::StructuralFallback;
358 }
359
360 match kind {
361 FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
362 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
363 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
364 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
365 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
366 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
367 FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
368 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
369 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
370 FieldKind::Text => LeafCodec::Scalar(ScalarCodec::Text),
371 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
372 FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
373 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
374 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
375 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
376 FieldKind::Account
377 | FieldKind::Decimal { .. }
378 | FieldKind::Enum { .. }
379 | FieldKind::Int128
380 | FieldKind::IntBig
381 | FieldKind::List(_)
382 | FieldKind::Map { .. }
383 | FieldKind::Set(_)
384 | FieldKind::Structured { .. }
385 | FieldKind::Uint128
386 | FieldKind::UintBig => LeafCodec::StructuralFallback,
387 }
388}
389
390#[derive(Clone, Copy, Debug, Eq, PartialEq)]
397pub enum RelationStrength {
398 Strong,
399 Weak,
400}
401
402#[derive(Clone, Copy, Debug)]
412pub enum FieldKind {
413 Account,
415 Blob,
416 Bool,
417 Date,
418 Decimal {
419 scale: u32,
421 },
422 Duration,
423 Enum {
424 path: &'static str,
426 variants: &'static [EnumVariantModel],
428 },
429 Float32,
430 Float64,
431 Int,
432 Int128,
433 IntBig,
434 Principal,
435 Subaccount,
436 Text,
437 Timestamp,
438 Uint,
439 Uint128,
440 UintBig,
441 Ulid,
442 Unit,
443
444 Relation {
447 target_path: &'static str,
449 target_entity_name: &'static str,
451 target_entity_tag: EntityTag,
453 target_store_path: &'static str,
455 key_kind: &'static Self,
456 strength: RelationStrength,
457 },
458
459 List(&'static Self),
461 Set(&'static Self),
462 Map {
466 key: &'static Self,
467 value: &'static Self,
468 },
469
470 Structured {
474 queryable: bool,
475 },
476}
477
478impl FieldKind {
479 #[must_use]
480 pub const fn value_kind(&self) -> RuntimeValueKind {
481 match self {
482 Self::Account
483 | Self::Blob
484 | Self::Bool
485 | Self::Date
486 | Self::Duration
487 | Self::Enum { .. }
488 | Self::Float32
489 | Self::Float64
490 | Self::Int
491 | Self::Int128
492 | Self::IntBig
493 | Self::Principal
494 | Self::Subaccount
495 | Self::Text
496 | Self::Timestamp
497 | Self::Uint
498 | Self::Uint128
499 | Self::UintBig
500 | Self::Ulid
501 | Self::Unit
502 | Self::Decimal { .. }
503 | Self::Relation { .. } => RuntimeValueKind::Atomic,
504 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
505 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
506 Self::Structured { queryable } => RuntimeValueKind::Structured {
507 queryable: *queryable,
508 },
509 }
510 }
511
512 #[must_use]
520 pub const fn is_deterministic_collection_shape(&self) -> bool {
521 match self {
522 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
523
524 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
525
526 Self::Map { key, value } => {
527 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
528 }
529
530 _ => true,
531 }
532 }
533
534 #[must_use]
537 pub(crate) fn supports_group_probe(&self) -> bool {
538 match self {
539 Self::Enum { variants, .. } => variants.iter().all(|variant| {
540 variant
541 .payload_kind()
542 .is_none_or(Self::supports_group_probe)
543 }),
544 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
545 Self::List(_)
546 | Self::Set(_)
547 | Self::Map { .. }
548 | Self::Structured { .. }
549 | Self::Unit => false,
550 Self::Account
551 | Self::Blob
552 | Self::Bool
553 | Self::Date
554 | Self::Decimal { .. }
555 | Self::Duration
556 | Self::Float32
557 | Self::Float64
558 | Self::Int
559 | Self::Int128
560 | Self::IntBig
561 | Self::Principal
562 | Self::Subaccount
563 | Self::Text
564 | Self::Timestamp
565 | Self::Uint
566 | Self::Uint128
567 | Self::UintBig
568 | Self::Ulid => true,
569 }
570 }
571
572 #[must_use]
578 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
579 match (self, value) {
580 (Self::Account, Value::Account(_))
581 | (Self::Blob, Value::Blob(_))
582 | (Self::Bool, Value::Bool(_))
583 | (Self::Date, Value::Date(_))
584 | (Self::Decimal { .. }, Value::Decimal(_))
585 | (Self::Duration, Value::Duration(_))
586 | (Self::Enum { .. }, Value::Enum(_))
587 | (Self::Float32, Value::Float32(_))
588 | (Self::Float64, Value::Float64(_))
589 | (Self::Int, Value::Int(_))
590 | (Self::Int128, Value::Int128(_))
591 | (Self::IntBig, Value::IntBig(_))
592 | (Self::Principal, Value::Principal(_))
593 | (Self::Subaccount, Value::Subaccount(_))
594 | (Self::Text, Value::Text(_))
595 | (Self::Timestamp, Value::Timestamp(_))
596 | (Self::Uint, Value::Uint(_))
597 | (Self::Uint128, Value::Uint128(_))
598 | (Self::UintBig, Value::UintBig(_))
599 | (Self::Ulid, Value::Ulid(_))
600 | (Self::Unit, Value::Unit)
601 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
602 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
603 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
604 items.iter().all(|item| inner.accepts_value(item))
605 }
606 (Self::Map { key, value }, Value::Map(entries)) => {
607 if Value::validate_map_entries(entries.as_slice()).is_err() {
608 return false;
609 }
610
611 entries.iter().all(|(entry_key, entry_value)| {
612 key.accepts_value(entry_key) && value.accepts_value(entry_value)
613 })
614 }
615 _ => false,
616 }
617 }
618}
619
620fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
626 match (kind, value) {
627 (FieldKind::Relation { key_kind, .. }, value) => {
628 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
629 }
630 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
631 .iter()
632 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
633 (
634 FieldKind::Map {
635 key,
636 value: value_kind,
637 },
638 Value::Map(entries),
639 ) => {
640 if Value::validate_map_entries(entries.as_slice()).is_err() {
641 return false;
642 }
643
644 entries.iter().all(|(entry_key, entry_value)| {
645 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
646 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
647 })
648 }
649 (FieldKind::Structured { .. }, _) => false,
650 _ => kind.accepts_value(value),
651 }
652}
653
654fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
658 match (kind, value) {
659 (FieldKind::Structured { .. }, _) => true,
660 (FieldKind::Relation { key_kind, .. }, value) => {
661 value_storage_kind_accepts_runtime_value(*key_kind, value)
662 }
663 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
664 .iter()
665 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
666 (
667 FieldKind::Map {
668 key,
669 value: value_kind,
670 },
671 Value::Map(entries),
672 ) => {
673 if Value::validate_map_entries(entries.as_slice()).is_err() {
674 return false;
675 }
676
677 entries.iter().all(|(entry_key, entry_value)| {
678 value_storage_kind_accepts_runtime_value(*key, entry_key)
679 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
680 })
681 }
682 _ => kind.accepts_value(value),
683 }
684}
685
686fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
689 if matches!(value, Value::Null) {
690 return Ok(());
691 }
692
693 match (kind, value) {
694 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
695 if decimal.scale() != scale {
696 return Err(format!(
697 "decimal scale mismatch: expected {scale}, found {}",
698 decimal.scale()
699 ));
700 }
701
702 Ok(())
703 }
704 (FieldKind::Relation { key_kind, .. }, value) => {
705 ensure_decimal_scale_matches(*key_kind, value)
706 }
707 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
708 for item in items {
709 ensure_decimal_scale_matches(*inner, item)?;
710 }
711
712 Ok(())
713 }
714 (
715 FieldKind::Map {
716 key,
717 value: map_value,
718 },
719 Value::Map(entries),
720 ) => {
721 for (entry_key, entry_value) in entries {
722 ensure_decimal_scale_matches(*key, entry_key)?;
723 ensure_decimal_scale_matches(*map_value, entry_value)?;
724 }
725
726 Ok(())
727 }
728 _ => Ok(()),
729 }
730}
731
732fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
735 match (kind, value) {
736 (FieldKind::Set(_), Value::List(items)) => {
737 for pair in items.windows(2) {
738 let [left, right] = pair else {
739 continue;
740 };
741 if Value::canonical_cmp(left, right) != Ordering::Less {
742 return Err("set payload must already be canonical and deduplicated".into());
743 }
744 }
745
746 Ok(())
747 }
748 (FieldKind::Map { .. }, Value::Map(entries)) => {
749 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
750
751 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
752 return Err("map payload must already be canonical and deduplicated".into());
753 }
754
755 Ok(())
756 }
757 _ => Ok(()),
758 }
759}