1use crate::{
7 db::schema::CompositeCodec,
8 db::{
9 data::decode_admitted_value_from_accepted_field_contract,
10 schema::{
11 AcceptedConstraintKind, AcceptedFieldKind, AcceptedFieldPersistenceContract,
12 AcceptedIdentityInspection, AcceptedInsertOmissionPolicy,
13 AcceptedRowLayoutRuntimeContract, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
14 ConstraintActivationKind, ConstraintActivationSnapshot, ConstraintActivationState,
15 ConstraintOrigin, ConstraintValidationJob, FieldId, PersistedIndexKeyItemSnapshot,
16 PersistedIndexKeySnapshot, PersistedNestedLeafSnapshot, PersistedSchemaSnapshot,
17 SchemaHistoricalFill,
18 composite_catalog::{AcceptedCompositeElement, AcceptedCompositeShape},
19 field_type_from_persisted_kind, identity_kind_maximum, output_value_from_runtime,
20 render_accepted_check_expr_sql,
21 runtime::AcceptedRowLayoutRuntimeField,
22 },
23 },
24 error::InternalError,
25 value::{OutputValue, render_output_value_text},
26};
27use std::fmt::Write;
28
29use candid::CandidType;
30use serde::Deserialize;
31use sha2::{Digest, Sha256};
32
33const ENTITY_FIELD_DESCRIPTION_NO_SLOT: u16 = u16::MAX;
34const MAX_SCHEMA_VALUE_RENDER_CHARS: usize = 128;
35
36#[cfg_attr(
37 doc,
38 doc = "EntitySchemaDescription\n\nStable describe payload for one entity model."
39)]
40#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
41pub struct EntitySchemaDescription {
42 pub(crate) entity_path: String,
43 pub(crate) entity_name: String,
44 pub(crate) entity_tag: u64,
45 pub(crate) accepted_schema_fingerprint_method: u8,
46 pub(crate) accepted_schema_fingerprint: [u8; 16],
47 pub(crate) primary_key: String,
48 pub(crate) primary_key_fields: Vec<String>,
49 pub(crate) identity: Option<Box<EntityIdentityDescription>>,
50 pub(crate) fields: Vec<EntityFieldDescription>,
51 pub(crate) indexes: Vec<EntityIndexDescription>,
52 pub(crate) relations: Vec<EntityRelationDescription>,
53 pub(crate) constraints: Vec<EntityConstraintDescription>,
54 pub(crate) row_layout_current: u32,
55 pub(crate) row_layout_history_floor: u32,
56}
57
58impl EntitySchemaDescription {
59 #[expect(
61 clippy::too_many_arguments,
62 reason = "schema description construction keeps identity, collections, and layout explicit"
63 )]
64 #[must_use]
65 pub const fn new(
66 entity_path: String,
67 entity_name: String,
68 entity_tag: u64,
69 accepted_schema_fingerprint_method: u8,
70 accepted_schema_fingerprint: [u8; 16],
71 primary_key: String,
72 primary_key_fields: Vec<String>,
73 fields: Vec<EntityFieldDescription>,
74 indexes: Vec<EntityIndexDescription>,
75 relations: Vec<EntityRelationDescription>,
76 constraints: Vec<EntityConstraintDescription>,
77 row_layout_current: u32,
78 row_layout_history_floor: u32,
79 ) -> Self {
80 Self {
81 entity_path,
82 entity_name,
83 entity_tag,
84 accepted_schema_fingerprint_method,
85 accepted_schema_fingerprint,
86 primary_key,
87 primary_key_fields,
88 identity: None,
89 fields,
90 indexes,
91 relations,
92 constraints,
93 row_layout_current,
94 row_layout_history_floor,
95 }
96 }
97
98 #[must_use]
100 pub const fn entity_path(&self) -> &str {
101 self.entity_path.as_str()
102 }
103
104 #[must_use]
106 pub const fn entity_name(&self) -> &str {
107 self.entity_name.as_str()
108 }
109
110 #[must_use]
112 pub const fn entity_tag(&self) -> u64 {
113 self.entity_tag
114 }
115
116 #[must_use]
118 pub const fn accepted_schema_fingerprint_method(&self) -> u8 {
119 self.accepted_schema_fingerprint_method
120 }
121
122 #[must_use]
124 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
125 self.accepted_schema_fingerprint
126 }
127
128 #[must_use]
130 pub const fn primary_key(&self) -> &str {
131 self.primary_key.as_str()
132 }
133
134 #[must_use]
136 pub const fn primary_key_fields(&self) -> &[String] {
137 self.primary_key_fields.as_slice()
138 }
139
140 #[must_use]
142 pub fn identity(&self) -> Option<&EntityIdentityDescription> {
143 self.identity.as_deref()
144 }
145
146 #[must_use]
148 pub const fn fields(&self) -> &[EntityFieldDescription] {
149 self.fields.as_slice()
150 }
151
152 #[must_use]
154 pub const fn indexes(&self) -> &[EntityIndexDescription] {
155 self.indexes.as_slice()
156 }
157
158 #[must_use]
160 pub const fn relations(&self) -> &[EntityRelationDescription] {
161 self.relations.as_slice()
162 }
163
164 #[must_use]
166 pub const fn constraints(&self) -> &[EntityConstraintDescription] {
167 self.constraints.as_slice()
168 }
169
170 #[must_use]
172 pub const fn row_layout_current(&self) -> u32 {
173 self.row_layout_current
174 }
175
176 #[must_use]
178 pub const fn row_layout_history_floor(&self) -> u32 {
179 self.row_layout_history_floor
180 }
181
182 fn with_identity(mut self, identity: Option<EntityIdentityDescription>) -> Self {
183 self.identity = identity.map(Box::new);
184 self
185 }
186}
187
188#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
191pub struct EntityIdentityDescription {
192 field: String,
193 generator: String,
194 accepted_kind: String,
195 minimum: u128,
196 maximum: u128,
197 high_water: u128,
198 remaining: u128,
199 exhausted: bool,
200}
201
202impl EntityIdentityDescription {
203 pub(in crate::db) fn new(
204 field: String,
205 accepted_kind: String,
206 maximum: u128,
207 high_water: u128,
208 ) -> Result<Self, InternalError> {
209 let remaining = maximum
210 .checked_sub(high_water)
211 .ok_or_else(InternalError::identity_state_corruption)?;
212 Ok(Self {
213 field,
214 generator: "Identity::next".to_string(),
215 accepted_kind,
216 minimum: 1,
217 maximum,
218 high_water,
219 remaining,
220 exhausted: high_water == maximum,
221 })
222 }
223
224 #[must_use]
226 pub const fn field(&self) -> &str {
227 self.field.as_str()
228 }
229
230 #[must_use]
232 pub const fn generator(&self) -> &str {
233 self.generator.as_str()
234 }
235
236 #[must_use]
238 pub const fn accepted_kind(&self) -> &str {
239 self.accepted_kind.as_str()
240 }
241
242 #[must_use]
244 pub const fn minimum(&self) -> u128 {
245 self.minimum
246 }
247
248 #[must_use]
250 pub const fn maximum(&self) -> u128 {
251 self.maximum
252 }
253
254 #[must_use]
256 pub const fn high_water(&self) -> u128 {
257 self.high_water
258 }
259
260 #[must_use]
262 pub const fn remaining(&self) -> u128 {
263 self.remaining
264 }
265
266 #[must_use]
268 pub const fn exhausted(&self) -> bool {
269 self.exhausted
270 }
271}
272
273pub(in crate::db) fn describe_accepted_identity(
274 identity: &AcceptedIdentityInspection,
275 high_water: u128,
276) -> Result<EntityIdentityDescription, InternalError> {
277 let accepted_kind = describe_kind_name(identity.accepted_kind())
278 .ok_or_else(InternalError::identity_state_corruption)?;
279 let maximum = identity_kind_maximum(identity.accepted_kind())
280 .ok_or_else(InternalError::identity_state_corruption)?;
281 EntityIdentityDescription::new(
282 identity.field_name().to_string(),
283 accepted_kind.to_string(),
284 maximum,
285 high_water,
286 )
287}
288
289#[cfg_attr(
290 doc,
291 doc = "EntityConstraintDescription\n\nOne accepted structural constraint entry in a describe payload."
292)]
293#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
294pub struct EntityConstraintDescription {
295 pub(crate) id: u32,
296 pub(crate) name: String,
297 pub(crate) kind: String,
298 pub(crate) origin: String,
299 pub(crate) validation_state: String,
300 pub(crate) validation_progress: Option<ConstraintValidationProgressDescription>,
301 pub(crate) field_id: Option<u32>,
302 pub(crate) index_id: Option<u32>,
303 pub(crate) relation_id: Option<u32>,
304 pub(crate) fields: Vec<String>,
305 pub(crate) index: Option<String>,
306 pub(crate) relation: Option<String>,
307 pub(crate) target_entity: Option<String>,
308 pub(crate) action: Option<String>,
309 pub(crate) semantics: String,
310 pub(crate) check_sql: Option<String>,
311}
312
313#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
315pub struct ConstraintValidationProgressDescription {
316 phase: String,
317 rows_scanned: u64,
318 findings_seen: u64,
319 restarts: u64,
320}
321
322impl ConstraintValidationProgressDescription {
323 fn from_job(job: &ConstraintValidationJob) -> Self {
324 Self {
325 phase: job.phase().as_str().to_string(),
326 rows_scanned: job.rows_scanned(),
327 findings_seen: job.findings_seen(),
328 restarts: job.restarts(),
329 }
330 }
331
332 #[must_use]
334 pub const fn phase(&self) -> &str {
335 self.phase.as_str()
336 }
337
338 #[must_use]
340 pub const fn rows_scanned(&self) -> u64 {
341 self.rows_scanned
342 }
343
344 #[must_use]
346 pub const fn findings_seen(&self) -> u64 {
347 self.findings_seen
348 }
349
350 #[must_use]
352 pub const fn restarts(&self) -> u64 {
353 self.restarts
354 }
355}
356
357impl EntityConstraintDescription {
358 #[must_use]
360 pub const fn id(&self) -> u32 {
361 self.id
362 }
363
364 #[must_use]
366 pub const fn name(&self) -> &str {
367 self.name.as_str()
368 }
369
370 #[must_use]
372 pub const fn kind(&self) -> &str {
373 self.kind.as_str()
374 }
375
376 #[must_use]
378 pub const fn origin(&self) -> &str {
379 self.origin.as_str()
380 }
381
382 #[must_use]
384 pub const fn validation_state(&self) -> &str {
385 self.validation_state.as_str()
386 }
387
388 #[must_use]
390 pub const fn validation_progress(&self) -> Option<&ConstraintValidationProgressDescription> {
391 self.validation_progress.as_ref()
392 }
393
394 #[must_use]
396 pub const fn field_id(&self) -> Option<u32> {
397 self.field_id
398 }
399
400 #[must_use]
402 pub const fn index_id(&self) -> Option<u32> {
403 self.index_id
404 }
405
406 #[must_use]
408 pub const fn relation_id(&self) -> Option<u32> {
409 self.relation_id
410 }
411
412 #[must_use]
414 pub const fn fields(&self) -> &[String] {
415 self.fields.as_slice()
416 }
417
418 #[must_use]
420 pub fn index(&self) -> Option<&str> {
421 self.index.as_deref()
422 }
423
424 #[must_use]
426 pub fn relation(&self) -> Option<&str> {
427 self.relation.as_deref()
428 }
429
430 #[must_use]
432 pub fn target_entity(&self) -> Option<&str> {
433 self.target_entity.as_deref()
434 }
435
436 #[must_use]
438 pub fn action(&self) -> Option<&str> {
439 self.action.as_deref()
440 }
441
442 #[must_use]
444 pub const fn semantics(&self) -> &str {
445 self.semantics.as_str()
446 }
447
448 #[must_use]
450 pub fn check_sql(&self) -> Option<&str> {
451 self.check_sql.as_deref()
452 }
453}
454
455#[cfg_attr(
456 doc,
457 doc = "EntityFieldDescription\n\nOne field entry in a describe payload."
458)]
459#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
460pub struct EntityFieldDescription {
461 pub(crate) name: String,
462 pub(crate) slot: u16,
463 pub(crate) kind: String,
464 pub(crate) nullable: bool,
465 pub(crate) primary_key: bool,
466 pub(crate) queryable: bool,
467 pub(crate) origin: String,
468 pub(crate) insert_omission: Option<String>,
469 pub(crate) insert_default: Option<String>,
470 pub(crate) insert_default_bytes: Option<u32>,
471 pub(crate) insert_default_hash: Option<String>,
472 pub(crate) introduced_in_layout: Option<u32>,
473 pub(crate) historical_fill: Option<String>,
474 pub(crate) historical_fill_bytes: Option<u32>,
475 pub(crate) historical_fill_hash: Option<String>,
476}
477
478struct EntityFieldTemporalFacts {
486 insert_omission: Option<String>,
487 insert_default: Option<String>,
488 insert_default_bytes: Option<u32>,
489 insert_default_hash: Option<String>,
490 introduced_in_layout: Option<u32>,
491 historical_fill: Option<String>,
492 historical_fill_bytes: Option<u32>,
493 historical_fill_hash: Option<String>,
494}
495
496impl EntityFieldTemporalFacts {
497 const fn nested() -> Self {
498 Self {
499 insert_omission: None,
500 insert_default: None,
501 insert_default_bytes: None,
502 insert_default_hash: None,
503 introduced_in_layout: None,
504 historical_fill: None,
505 historical_fill_bytes: None,
506 historical_fill_hash: None,
507 }
508 }
509}
510
511impl EntityFieldDescription {
512 #[expect(
514 clippy::too_many_arguments,
515 reason = "schema description construction keeps every temporal field fact explicit"
516 )]
517 #[must_use]
518 pub fn new(
519 name: String,
520 slot: Option<u16>,
521 kind: String,
522 nullable: bool,
523 primary_key: bool,
524 queryable: bool,
525 origin: String,
526 insert_omission: Option<String>,
527 insert_default: Option<String>,
528 insert_default_bytes: Option<u32>,
529 insert_default_hash: Option<String>,
530 introduced_in_layout: Option<u32>,
531 historical_fill: Option<String>,
532 historical_fill_bytes: Option<u32>,
533 historical_fill_hash: Option<String>,
534 ) -> Self {
535 Self::new_with_temporal_facts(
536 name,
537 slot,
538 primary_key,
539 DescribeFieldMetadata::new(kind, nullable, queryable, origin),
540 EntityFieldTemporalFacts {
541 insert_omission,
542 insert_default,
543 insert_default_bytes,
544 insert_default_hash,
545 introduced_in_layout,
546 historical_fill,
547 historical_fill_bytes,
548 historical_fill_hash,
549 },
550 )
551 }
552
553 fn new_with_temporal_facts(
554 name: String,
555 slot: Option<u16>,
556 primary_key: bool,
557 metadata: DescribeFieldMetadata,
558 temporal: EntityFieldTemporalFacts,
559 ) -> Self {
560 let slot = match slot {
561 Some(slot) => slot,
562 None => ENTITY_FIELD_DESCRIPTION_NO_SLOT,
563 };
564
565 Self {
566 name,
567 slot,
568 kind: metadata.kind,
569 nullable: metadata.nullable,
570 primary_key,
571 queryable: metadata.queryable,
572 origin: metadata.origin,
573 insert_omission: temporal.insert_omission,
574 insert_default: temporal.insert_default,
575 insert_default_bytes: temporal.insert_default_bytes,
576 insert_default_hash: temporal.insert_default_hash,
577 introduced_in_layout: temporal.introduced_in_layout,
578 historical_fill: temporal.historical_fill,
579 historical_fill_bytes: temporal.historical_fill_bytes,
580 historical_fill_hash: temporal.historical_fill_hash,
581 }
582 }
583
584 #[must_use]
586 pub const fn name(&self) -> &str {
587 self.name.as_str()
588 }
589
590 #[must_use]
592 pub const fn slot(&self) -> Option<u16> {
593 if self.slot == ENTITY_FIELD_DESCRIPTION_NO_SLOT {
594 None
595 } else {
596 Some(self.slot)
597 }
598 }
599
600 #[must_use]
602 pub const fn kind(&self) -> &str {
603 self.kind.as_str()
604 }
605
606 #[must_use]
608 pub const fn nullable(&self) -> bool {
609 self.nullable
610 }
611
612 #[must_use]
614 pub const fn primary_key(&self) -> bool {
615 self.primary_key
616 }
617
618 #[must_use]
620 pub const fn queryable(&self) -> bool {
621 self.queryable
622 }
623
624 #[must_use]
626 pub const fn origin(&self) -> &str {
627 self.origin.as_str()
628 }
629
630 #[must_use]
632 pub fn insert_omission(&self) -> Option<&str> {
633 self.insert_omission.as_deref()
634 }
635
636 #[must_use]
638 pub fn insert_default(&self) -> Option<&str> {
639 self.insert_default.as_deref()
640 }
641
642 #[must_use]
644 pub const fn insert_default_bytes(&self) -> Option<u32> {
645 self.insert_default_bytes
646 }
647
648 #[must_use]
650 pub fn insert_default_hash(&self) -> Option<&str> {
651 self.insert_default_hash.as_deref()
652 }
653
654 #[must_use]
656 pub const fn introduced_in_layout(&self) -> Option<u32> {
657 self.introduced_in_layout
658 }
659
660 #[must_use]
662 pub fn historical_fill(&self) -> Option<&str> {
663 self.historical_fill.as_deref()
664 }
665
666 #[must_use]
668 pub const fn historical_fill_bytes(&self) -> Option<u32> {
669 self.historical_fill_bytes
670 }
671
672 #[must_use]
674 pub fn historical_fill_hash(&self) -> Option<&str> {
675 self.historical_fill_hash.as_deref()
676 }
677}
678
679#[cfg_attr(
680 doc,
681 doc = "EntityIndexDescription\n\nOne index entry in a describe payload."
682)]
683#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
684pub struct EntityIndexDescription {
685 pub(crate) name: String,
686 pub(crate) unique: bool,
687 pub(crate) fields: Vec<String>,
688 pub(crate) origin: String,
689}
690
691impl EntityIndexDescription {
692 #[must_use]
694 pub const fn new(name: String, unique: bool, fields: Vec<String>, origin: String) -> Self {
695 Self {
696 name,
697 unique,
698 fields,
699 origin,
700 }
701 }
702
703 #[must_use]
705 pub const fn name(&self) -> &str {
706 self.name.as_str()
707 }
708
709 #[must_use]
711 pub const fn unique(&self) -> bool {
712 self.unique
713 }
714
715 #[must_use]
717 pub const fn fields(&self) -> &[String] {
718 self.fields.as_slice()
719 }
720
721 #[must_use]
723 pub const fn origin(&self) -> &str {
724 self.origin.as_str()
725 }
726}
727
728#[cfg_attr(
729 doc,
730 doc = "EntityRelationDescription\n\nOne relation entry in a describe payload."
731)]
732#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
733pub struct EntityRelationDescription {
734 pub(crate) field: String,
735 pub(crate) target_path: String,
736 pub(crate) target_entity_name: String,
737 pub(crate) target_store_path: String,
738 pub(crate) cardinality: EntityRelationCardinality,
739}
740
741impl EntityRelationDescription {
742 #[must_use]
744 pub const fn new(
745 field: String,
746 target_path: String,
747 target_entity_name: String,
748 target_store_path: String,
749 cardinality: EntityRelationCardinality,
750 ) -> Self {
751 Self {
752 field,
753 target_path,
754 target_entity_name,
755 target_store_path,
756 cardinality,
757 }
758 }
759
760 #[must_use]
762 pub const fn field(&self) -> &str {
763 self.field.as_str()
764 }
765
766 #[must_use]
768 pub const fn target_path(&self) -> &str {
769 self.target_path.as_str()
770 }
771
772 #[must_use]
774 pub const fn target_entity_name(&self) -> &str {
775 self.target_entity_name.as_str()
776 }
777
778 #[must_use]
780 pub const fn target_store_path(&self) -> &str {
781 self.target_store_path.as_str()
782 }
783
784 #[must_use]
786 pub const fn cardinality(&self) -> EntityRelationCardinality {
787 self.cardinality
788 }
789}
790
791#[cfg_attr(
792 doc,
793 doc = "EntityRelationCardinality\n\nDescribe relation cardinality."
794)]
795#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
796pub enum EntityRelationCardinality {
797 Single,
798 List,
799 Set,
800}
801
802pub(in crate::db) fn describe_accepted_entity_with_persisted_schema(
804 schema: &AcceptedSchemaSnapshot,
805 value_catalog: &AcceptedValueCatalogHandle,
806 validation_jobs: &[ConstraintValidationJob],
807 identity: Option<EntityIdentityDescription>,
808 entity_tag: u64,
809 accepted_schema_fingerprint_method: u8,
810 accepted_schema_fingerprint: [u8; 16],
811) -> Result<EntitySchemaDescription, InternalError> {
812 describe_entity_with_persisted_schema(
813 schema,
814 value_catalog,
815 validation_jobs,
816 identity,
817 entity_tag,
818 accepted_schema_fingerprint_method,
819 accepted_schema_fingerprint,
820 )
821}
822
823fn describe_entity_with_persisted_schema(
824 schema: &AcceptedSchemaSnapshot,
825 value_catalog: &AcceptedValueCatalogHandle,
826 validation_jobs: &[ConstraintValidationJob],
827 identity: Option<EntityIdentityDescription>,
828 entity_tag: u64,
829 accepted_schema_fingerprint_method: u8,
830 accepted_schema_fingerprint: [u8; 16],
831) -> Result<EntitySchemaDescription, InternalError> {
832 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
833 let fields = describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)?;
834 let primary_key_fields = schema.primary_key_field_names();
835 if primary_key_fields.is_empty() {
836 return Err(InternalError::store_invariant());
837 }
838 let primary_key_fields = primary_key_fields
839 .into_iter()
840 .map(str::to_string)
841 .collect::<Vec<_>>();
842 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
843
844 Ok(describe_entity_model_from_description_rows(
845 schema.entity_path(),
846 schema.entity_name(),
847 entity_tag,
848 accepted_schema_fingerprint_method,
849 accepted_schema_fingerprint,
850 primary_key.as_str(),
851 primary_key_fields,
852 fields,
853 describe_entity_indexes_with_persisted_schema(schema),
854 describe_entity_relations_with_persisted_schema(schema),
855 describe_entity_constraints_with_persisted_schema(schema, value_catalog, validation_jobs)?,
856 row_layout.current_layout_version().get(),
857 row_layout.history_floor().get(),
858 )
859 .with_identity(identity))
860}
861
862#[expect(
867 clippy::too_many_arguments,
868 reason = "one final schema DTO assembly keeps every already-owned section explicit"
869)]
870fn describe_entity_model_from_description_rows(
871 entity_path: &str,
872 entity_name: &str,
873 entity_tag: u64,
874 accepted_schema_fingerprint_method: u8,
875 accepted_schema_fingerprint: [u8; 16],
876 primary_key: &str,
877 primary_key_fields: Vec<String>,
878 fields: Vec<EntityFieldDescription>,
879 indexes: Vec<EntityIndexDescription>,
880 relations: Vec<EntityRelationDescription>,
881 constraints: Vec<EntityConstraintDescription>,
882 row_layout_current: u32,
883 row_layout_history_floor: u32,
884) -> EntitySchemaDescription {
885 EntitySchemaDescription::new(
886 entity_path.to_string(),
887 entity_name.to_string(),
888 entity_tag,
889 accepted_schema_fingerprint_method,
890 accepted_schema_fingerprint,
891 primary_key.to_string(),
892 primary_key_fields,
893 fields,
894 indexes,
895 relations,
896 constraints,
897 row_layout_current,
898 row_layout_history_floor,
899 )
900}
901
902fn describe_entity_constraints_with_persisted_schema(
903 schema: &AcceptedSchemaSnapshot,
904 value_catalog: &AcceptedValueCatalogHandle,
905 validation_jobs: &[ConstraintValidationJob],
906) -> Result<Vec<EntityConstraintDescription>, InternalError> {
907 let snapshot = schema.persisted_snapshot();
908 let mut descriptions = snapshot
909 .constraints()
910 .iter()
911 .map(|constraint| describe_accepted_constraint(snapshot, value_catalog, constraint))
912 .collect::<Result<Vec<_>, InternalError>>()?;
913 descriptions.extend(
914 snapshot
915 .constraint_activations()
916 .iter()
917 .map(|activation| {
918 let job = validation_jobs
919 .iter()
920 .find(|job| job.constraint_id() == activation.id());
921 describe_constraint_activation(snapshot, value_catalog, activation, job)
922 })
923 .collect::<Result<Vec<_>, InternalError>>()?,
924 );
925 if validation_jobs.iter().any(|job| {
926 !snapshot
927 .constraint_activations()
928 .iter()
929 .any(|activation| activation.id() == job.constraint_id())
930 }) {
931 return Err(InternalError::store_invariant());
932 }
933 descriptions.sort_unstable_by_key(|description| {
934 (
935 description.id(),
936 description.validation_state() != "validated",
937 )
938 });
939 Ok(descriptions)
940}
941
942fn describe_accepted_constraint(
943 snapshot: &PersistedSchemaSnapshot,
944 value_catalog: &AcceptedValueCatalogHandle,
945 constraint: &crate::db::schema::AcceptedConstraintSnapshot,
946) -> Result<EntityConstraintDescription, InternalError> {
947 let mut description = accepted_constraint_description(
948 constraint.id().get(),
949 constraint.name(),
950 constraint.origin(),
951 );
952 match constraint.kind() {
953 AcceptedConstraintKind::PrimaryKey => {
954 description.kind = "primary_key".to_string();
955 description.fields = snapshot
956 .primary_key_field_ids()
957 .iter()
958 .map(|field_id| accepted_field_name(snapshot, *field_id))
959 .collect::<Result<Vec<_>, _>>()?;
960 description.semantics = "primary_key_v1".to_string();
961 }
962 AcceptedConstraintKind::NotNull { field_id } => {
963 description.kind = "not_null".to_string();
964 description.field_id = Some(field_id.get());
965 description.fields = vec![accepted_field_name(snapshot, *field_id)?];
966 description.semantics = "not_null_v1".to_string();
967 }
968 AcceptedConstraintKind::Unique { index_id } => {
969 let index = snapshot
970 .indexes()
971 .iter()
972 .find(|index| index.schema_id() == *index_id)
973 .ok_or_else(InternalError::store_invariant)?;
974 description.kind = "unique".to_string();
975 description.index_id = Some(index_id.get());
976 description.fields = describe_persisted_index_fields(index.key());
977 description.index = Some(index.name().to_string());
978 description.semantics = "unique_index_v1".to_string();
979 }
980 AcceptedConstraintKind::Relation { relation_id } => {
981 let relation = snapshot
982 .relations()
983 .iter()
984 .find(|relation| relation.id() == *relation_id)
985 .ok_or_else(InternalError::store_invariant)?;
986 description.kind = "relation".to_string();
987 description.relation_id = Some(relation_id.get());
988 description.fields = relation
989 .local_field_ids()
990 .iter()
991 .map(|field_id| accepted_field_name(snapshot, *field_id))
992 .collect::<Result<Vec<_>, _>>()?;
993 description.relation = Some(relation.name().to_string());
994 description.target_entity = Some(relation.target_path().to_string());
995 description.action = Some("restrict".to_string());
996 description.semantics = "relation_pk_restrict_v1".to_string();
997 }
998 AcceptedConstraintKind::Check { expression } => {
999 description.kind = "check".to_string();
1000 description.fields = expression
1001 .dependencies()
1002 .into_iter()
1003 .map(|field_id| accepted_field_name(snapshot, field_id))
1004 .collect::<Result<Vec<_>, _>>()?;
1005 description.semantics = "check_expr_v1".to_string();
1006 description.check_sql = Some(render_accepted_check_expr_sql(
1007 expression,
1008 snapshot,
1009 value_catalog,
1010 )?);
1011 }
1012 AcceptedConstraintKind::TargetedRule { target, operation } => {
1013 description.kind = "targeted_rule".to_string();
1014 description.field_id = Some(target.root_field_id().get());
1015 description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1016 description.semantics = match operation.as_ref() {
1017 crate::db::schema::AcceptedRuleOperation::LengthRangeInclusive { .. } => {
1018 "targeted_length_range_v1"
1019 }
1020 crate::db::schema::AcceptedRuleOperation::MultipleOf { .. } => {
1021 "targeted_multiple_of_v1"
1022 }
1023 crate::db::schema::AcceptedRuleOperation::NumericMaximumInclusive { .. } => {
1024 "targeted_numeric_maximum_v1"
1025 }
1026 crate::db::schema::AcceptedRuleOperation::NumericMinimumInclusive { .. } => {
1027 "targeted_numeric_minimum_v1"
1028 }
1029 crate::db::schema::AcceptedRuleOperation::NumericRangeInclusive { .. } => {
1030 "targeted_numeric_range_v1"
1031 }
1032 }
1033 .to_string();
1034 }
1035 }
1036 Ok(description)
1037}
1038
1039fn describe_constraint_activation(
1040 snapshot: &PersistedSchemaSnapshot,
1041 value_catalog: &AcceptedValueCatalogHandle,
1042 activation: &ConstraintActivationSnapshot,
1043 validation_job: Option<&ConstraintValidationJob>,
1044) -> Result<EntityConstraintDescription, InternalError> {
1045 let mut description = accepted_constraint_description(
1046 activation.id().get(),
1047 activation.name(),
1048 activation.origin(),
1049 );
1050 match activation.state() {
1051 ConstraintActivationState::EnforcingNewWrites if validation_job.is_none() => {
1052 description.validation_state = "enforcing_new_writes".to_string();
1053 }
1054 ConstraintActivationState::Validating => {
1055 let job = validation_job.ok_or_else(InternalError::store_invariant)?;
1056 job.validate(Some(activation))?;
1057 description.validation_state = "validating".to_string();
1058 description.validation_progress =
1059 Some(ConstraintValidationProgressDescription::from_job(job));
1060 }
1061 ConstraintActivationState::EnforcingNewWrites => {
1062 return Err(InternalError::store_invariant());
1063 }
1064 }
1065 match activation.kind() {
1066 ConstraintActivationKind::NotNull { field_id } => {
1067 description.kind = "not_null".to_string();
1068 description.field_id = Some(field_id.get());
1069 description.fields = vec![accepted_field_name(snapshot, *field_id)?];
1070 description.semantics = "not_null_v1".to_string();
1071 }
1072 ConstraintActivationKind::Unique { index_id } => {
1073 let index = snapshot
1074 .candidate_indexes()
1075 .iter()
1076 .find(|index| index.schema_id() == *index_id)
1077 .ok_or_else(InternalError::store_invariant)?;
1078 description.kind = "unique".to_string();
1079 description.index_id = Some(index_id.get());
1080 description.fields = describe_persisted_index_fields(index.key());
1081 description.index = Some(index.name().to_string());
1082 description.semantics = "unique_index_v1".to_string();
1083 }
1084 ConstraintActivationKind::Relation { relation_id } => {
1085 let relation = snapshot
1086 .candidate_relations()
1087 .iter()
1088 .find(|relation| relation.id() == *relation_id)
1089 .ok_or_else(InternalError::store_invariant)?;
1090 description.kind = "relation".to_string();
1091 description.relation_id = Some(relation_id.get());
1092 description.fields = relation
1093 .local_field_ids()
1094 .iter()
1095 .map(|field_id| accepted_field_name(snapshot, *field_id))
1096 .collect::<Result<Vec<_>, _>>()?;
1097 description.relation = Some(relation.name().to_string());
1098 description.target_entity = Some(relation.target_path().to_string());
1099 description.action = Some("restrict".to_string());
1100 description.semantics = "relation_pk_restrict_v1".to_string();
1101 }
1102 ConstraintActivationKind::Check { expression } => {
1103 description.kind = "check".to_string();
1104 description.fields = expression
1105 .dependencies()
1106 .into_iter()
1107 .map(|field_id| accepted_field_name(snapshot, field_id))
1108 .collect::<Result<Vec<_>, _>>()?;
1109 description.semantics = "check_expr_v1".to_string();
1110 description.check_sql = Some(render_accepted_check_expr_sql(
1111 expression,
1112 snapshot,
1113 value_catalog,
1114 )?);
1115 }
1116 ConstraintActivationKind::TargetedRule { target, operation } => {
1117 description.kind = "targeted_rule".to_string();
1118 description.field_id = Some(target.root_field_id().get());
1119 description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1120 description.semantics = match operation.as_ref() {
1121 crate::db::schema::AcceptedRuleOperation::LengthRangeInclusive { .. } => {
1122 "targeted_length_range_v1"
1123 }
1124 crate::db::schema::AcceptedRuleOperation::MultipleOf { .. } => {
1125 "targeted_multiple_of_v1"
1126 }
1127 crate::db::schema::AcceptedRuleOperation::NumericMaximumInclusive { .. } => {
1128 "targeted_numeric_maximum_v1"
1129 }
1130 crate::db::schema::AcceptedRuleOperation::NumericMinimumInclusive { .. } => {
1131 "targeted_numeric_minimum_v1"
1132 }
1133 crate::db::schema::AcceptedRuleOperation::NumericRangeInclusive { .. } => {
1134 "targeted_numeric_range_v1"
1135 }
1136 }
1137 .to_string();
1138 }
1139 }
1140 Ok(description)
1141}
1142
1143fn accepted_constraint_description(
1144 id: u32,
1145 name: &str,
1146 origin: ConstraintOrigin,
1147) -> EntityConstraintDescription {
1148 EntityConstraintDescription {
1149 id,
1150 name: name.to_string(),
1151 kind: String::new(),
1152 origin: accepted_constraint_origin_label(origin).to_string(),
1153 validation_state: "validated".to_string(),
1154 validation_progress: None,
1155 field_id: None,
1156 index_id: None,
1157 relation_id: None,
1158 fields: Vec::new(),
1159 index: None,
1160 relation: None,
1161 target_entity: None,
1162 action: None,
1163 semantics: String::new(),
1164 check_sql: None,
1165 }
1166}
1167
1168const fn accepted_constraint_origin_label(origin: ConstraintOrigin) -> &'static str {
1169 match origin {
1170 ConstraintOrigin::Generated => "generated",
1171 ConstraintOrigin::SqlDdl => "sql_ddl",
1172 }
1173}
1174
1175fn accepted_field_name(
1176 snapshot: &crate::db::schema::PersistedSchemaSnapshot,
1177 field_id: FieldId,
1178) -> Result<String, InternalError> {
1179 snapshot
1180 .fields()
1181 .iter()
1182 .find(|field| field.id() == field_id)
1183 .map(|field| field.name().to_string())
1184 .ok_or_else(InternalError::store_invariant)
1185}
1186
1187fn render_primary_key_fields(fields: &[String]) -> String {
1188 fields.join(", ")
1189}
1190
1191fn describe_entity_indexes_with_persisted_schema(
1192 schema: &AcceptedSchemaSnapshot,
1193) -> Vec<EntityIndexDescription> {
1194 schema
1195 .persisted_snapshot()
1196 .indexes()
1197 .iter()
1198 .map(|index| {
1199 EntityIndexDescription::new(
1200 index.name().to_string(),
1201 index.unique(),
1202 describe_persisted_index_fields(index.key()),
1203 if index.generated() {
1204 "generated".to_string()
1205 } else {
1206 "ddl".to_string()
1207 },
1208 )
1209 })
1210 .collect()
1211}
1212
1213fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
1214 match key {
1215 PersistedIndexKeySnapshot::FieldPath(paths) => paths
1216 .iter()
1217 .map(|field_path| field_path.path().join("."))
1218 .collect(),
1219 PersistedIndexKeySnapshot::Items(items) => items
1220 .iter()
1221 .map(|item| match item {
1222 PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
1223 PersistedIndexKeyItemSnapshot::Expression(expression) => {
1224 expression.canonical_text().to_string()
1225 }
1226 })
1227 .collect(),
1228 }
1229}
1230
1231#[cfg_attr(
1232 doc,
1233 doc = "Build field descriptors using accepted persisted schema slot metadata."
1234)]
1235#[cfg(any(test, feature = "sql"))]
1236pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1237 schema: &AcceptedSchemaSnapshot,
1238 value_catalog: &AcceptedValueCatalogHandle,
1239) -> Result<Vec<EntityFieldDescription>, InternalError> {
1240 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1241 describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1242}
1243
1244fn describe_entity_fields_with_runtime_contract(
1245 schema: &AcceptedSchemaSnapshot,
1246 row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1247 value_catalog: &AcceptedValueCatalogHandle,
1248) -> Result<Vec<EntityFieldDescription>, InternalError> {
1249 let snapshot = schema.persisted_snapshot();
1250 if snapshot.fields().len() != row_layout.fields().len() {
1251 return Err(InternalError::store_invariant());
1252 }
1253 let mut fields = Vec::with_capacity(snapshot.fields().len());
1254
1255 for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1258 if field.id() != runtime_field.field_id() {
1259 return Err(InternalError::store_invariant());
1260 }
1261 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1262 let slot = Some(runtime_field.slot().get());
1263 let metadata = DescribeFieldMetadata::new(
1264 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1265 field.nullable(),
1266 field_type_from_persisted_kind(field.kind()).is_queryable(),
1267 field_origin_label(field.generated()),
1268 );
1269 let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1270
1271 push_described_field_row(
1272 &mut fields,
1273 field.name(),
1274 slot,
1275 primary_key,
1276 None,
1277 metadata,
1278 temporal,
1279 );
1280
1281 if !field.nested_leaves().is_empty() {
1282 describe_persisted_nested_leaves(
1283 &mut fields,
1284 field.nested_leaves(),
1285 field_origin_label(field.generated()),
1286 value_catalog,
1287 )?;
1288 }
1289 }
1290
1291 Ok(fields)
1292}
1293
1294struct DescribeFieldMetadata {
1301 kind: String,
1302 nullable: bool,
1303 queryable: bool,
1304 origin: String,
1305}
1306
1307impl DescribeFieldMetadata {
1308 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1310 Self {
1311 kind,
1312 nullable,
1313 queryable,
1314 origin,
1315 }
1316 }
1317}
1318
1319fn push_described_field_row(
1322 fields: &mut Vec<EntityFieldDescription>,
1323 name: &str,
1324 slot: Option<u16>,
1325 primary_key: bool,
1326 tree_prefix: Option<&'static str>,
1327 metadata: DescribeFieldMetadata,
1328 temporal: EntityFieldTemporalFacts,
1329) {
1330 let display_name = if let Some(prefix) = tree_prefix {
1333 format!("{prefix}{name}")
1334 } else {
1335 name.to_string()
1336 };
1337
1338 fields.push(EntityFieldDescription::new_with_temporal_facts(
1339 display_name,
1340 slot,
1341 primary_key,
1342 metadata,
1343 temporal,
1344 ));
1345}
1346
1347fn describe_persisted_nested_leaves(
1350 fields: &mut Vec<EntityFieldDescription>,
1351 nested_leaves: &[PersistedNestedLeafSnapshot],
1352 origin: String,
1353 value_catalog: &AcceptedValueCatalogHandle,
1354) -> Result<(), InternalError> {
1355 for (index, leaf) in nested_leaves.iter().enumerate() {
1356 let prefix = if index + 1 == nested_leaves.len() {
1357 "└─ "
1358 } else {
1359 "├─ "
1360 };
1361 let name = leaf.path().last().map_or("", String::as_str);
1362 let metadata = DescribeFieldMetadata::new(
1363 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1364 leaf.nullable(),
1365 field_type_from_persisted_kind(leaf.kind()).is_queryable(),
1366 origin.clone(),
1367 );
1368
1369 push_described_field_row(
1370 fields,
1371 name,
1372 None,
1373 false,
1374 Some(prefix),
1375 metadata,
1376 EntityFieldTemporalFacts::nested(),
1377 );
1378 }
1379
1380 Ok(())
1381}
1382
1383fn field_origin_label(generated: bool) -> String {
1384 if generated {
1385 "generated".to_string()
1386 } else {
1387 "ddl".to_string()
1388 }
1389}
1390
1391fn describe_entity_relations_with_persisted_schema(
1392 schema: &AcceptedSchemaSnapshot,
1393) -> Vec<EntityRelationDescription> {
1394 schema
1395 .persisted_snapshot()
1396 .fields()
1397 .iter()
1398 .filter_map(relation_description_from_persisted_field)
1399 .collect()
1400}
1401
1402fn relation_description_from_persisted_field(
1403 field: &crate::db::schema::PersistedFieldSnapshot,
1404) -> Option<EntityRelationDescription> {
1405 let relation = persisted_relation_description_metadata(field.kind())?;
1406
1407 Some(EntityRelationDescription::new(
1408 field.name().to_string(),
1409 relation.target_path.to_string(),
1410 relation.target_entity_name.to_string(),
1411 relation.target_store_path.to_string(),
1412 relation.cardinality,
1413 ))
1414}
1415
1416struct PersistedRelationDescriptionMetadata<'a> {
1417 target_path: &'a str,
1418 target_entity_name: &'a str,
1419 target_store_path: &'a str,
1420 cardinality: EntityRelationCardinality,
1421}
1422
1423fn persisted_relation_description_metadata(
1424 kind: &AcceptedFieldKind,
1425) -> Option<PersistedRelationDescriptionMetadata<'_>> {
1426 const fn from_relation_kind(
1427 kind: &AcceptedFieldKind,
1428 cardinality: EntityRelationCardinality,
1429 ) -> Option<PersistedRelationDescriptionMetadata<'_>> {
1430 let AcceptedFieldKind::Relation {
1431 target_path,
1432 target_entity_name,
1433 target_store_path,
1434 ..
1435 } = kind
1436 else {
1437 return None;
1438 };
1439
1440 Some(PersistedRelationDescriptionMetadata {
1441 target_path: target_path.as_str(),
1442 target_entity_name: target_entity_name.as_str(),
1443 target_store_path: target_store_path.as_str(),
1444 cardinality,
1445 })
1446 }
1447
1448 match kind {
1449 AcceptedFieldKind::Relation { .. } => {
1450 from_relation_kind(kind, EntityRelationCardinality::Single)
1451 }
1452 AcceptedFieldKind::List(inner) => {
1453 from_relation_kind(inner, EntityRelationCardinality::List)
1454 }
1455 AcceptedFieldKind::Set(inner) => from_relation_kind(inner, EntityRelationCardinality::Set),
1456 _ => None,
1457 }
1458}
1459
1460fn write_accepted_composite_shape_summary(
1461 out: &mut String,
1462 shape: &AcceptedCompositeShape,
1463 value_catalog: &AcceptedValueCatalogHandle,
1464) -> Result<(), InternalError> {
1465 match shape {
1466 AcceptedCompositeShape::Record(fields) => {
1467 out.push_str("record{");
1468 for (index, field) in fields.iter().enumerate() {
1469 if index > 0 {
1470 out.push_str(", ");
1471 }
1472 out.push_str(field.name());
1473 out.push(':');
1474 write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1475 }
1476 out.push('}');
1477 }
1478 AcceptedCompositeShape::Tuple(elements) => {
1479 out.push_str("tuple<");
1480 for (index, element) in elements.iter().enumerate() {
1481 if index > 0 {
1482 out.push_str(", ");
1483 }
1484 write_accepted_composite_element_summary(out, element, value_catalog)?;
1485 }
1486 out.push('>');
1487 }
1488 AcceptedCompositeShape::Newtype(inner) => {
1489 out.push_str("newtype<");
1490 write_accepted_composite_element_summary(out, inner, value_catalog)?;
1491 out.push('>');
1492 }
1493 }
1494
1495 Ok(())
1496}
1497
1498fn write_accepted_composite_element_summary(
1499 out: &mut String,
1500 element: &AcceptedCompositeElement,
1501 value_catalog: &AcceptedValueCatalogHandle,
1502) -> Result<(), InternalError> {
1503 write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1504 write_composite_nullability_summary(out, element.nullable());
1505 Ok(())
1506}
1507
1508fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1509 match codec {
1510 CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1511 }
1512}
1513
1514fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1515 if nullable {
1516 out.push('?');
1517 }
1518}
1519
1520fn write_length_bounded_field_kind_summary(
1524 out: &mut String,
1525 kind_name: &str,
1526 max_len: Option<u32>,
1527) {
1528 out.push_str(kind_name);
1529 if let Some(max_len) = max_len {
1530 out.push_str("(max_len=");
1531 out.push_str(&max_len.to_string());
1532 out.push(')');
1533 } else {
1534 out.push_str("(unbounded)");
1535 }
1536}
1537
1538fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1539 out.push_str(kind_name);
1540 out.push_str("(max_bytes=");
1541 out.push_str(&max_bytes.to_string());
1542 out.push(')');
1543}
1544
1545struct RenderedTemporalPayload {
1553 value: String,
1554 bytes: u32,
1555 hash: String,
1556}
1557
1558fn accepted_field_temporal_facts(
1559 field: &AcceptedRowLayoutRuntimeField<'_>,
1560 value_catalog: &AcceptedValueCatalogHandle,
1561) -> Result<EntityFieldTemporalFacts, InternalError> {
1562 let write_policy = field.write_policy();
1563 let insert_omission = if write_policy.insert_generation().is_some() {
1564 "generated"
1565 } else if write_policy.write_management().is_some() {
1566 "managed"
1567 } else {
1568 match field.insert_omission_policy() {
1569 AcceptedInsertOmissionPolicy::NullIfMissing => "null",
1570 AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
1571 AcceptedInsertOmissionPolicy::Required => "required",
1572 }
1573 };
1574 let insert_default = field
1575 .insert_default()
1576 .slot_payload()
1577 .map(|payload| accepted_payload_facts(field, value_catalog, payload))
1578 .transpose()?;
1579 let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
1580 Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
1581 None => (None, None, None),
1582 };
1583 let (historical_fill, historical_fill_bytes, historical_fill_hash) =
1584 match field.historical_fill() {
1585 SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
1586 SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
1587 SchemaHistoricalFill::SlotPayload(payload) => {
1588 let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
1589 (
1590 Some(rendered.value),
1591 Some(rendered.bytes),
1592 Some(rendered.hash),
1593 )
1594 }
1595 };
1596
1597 Ok(EntityFieldTemporalFacts {
1598 insert_omission: Some(insert_omission.to_string()),
1599 insert_default,
1600 insert_default_bytes,
1601 insert_default_hash,
1602 introduced_in_layout: Some(field.introduced_in_layout().get()),
1603 historical_fill,
1604 historical_fill_bytes,
1605 historical_fill_hash,
1606 })
1607}
1608
1609fn accepted_payload_facts(
1610 field: &AcceptedRowLayoutRuntimeField<'_>,
1611 value_catalog: &AcceptedValueCatalogHandle,
1612 payload: &[u8],
1613) -> Result<RenderedTemporalPayload, InternalError> {
1614 let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
1615 .map_err(|_| InternalError::store_invariant())?;
1616 let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
1617 let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.value())
1618 .map_err(|_| InternalError::store_invariant())?;
1619 let hash = short_default_payload_fingerprint(payload);
1620 let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
1621 let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
1622
1623 Ok(RenderedTemporalPayload {
1624 value: rendered,
1625 bytes,
1626 hash,
1627 })
1628}
1629
1630fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
1631 let rendered = match value {
1632 OutputValue::Text(value) => format!("'{}'", value.escape_default()),
1633 _ => render_output_value_text(value),
1634 };
1635 if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
1636 return rendered;
1637 }
1638
1639 format!(
1640 "{}(bytes={}, sha256={})",
1641 output_value_kind_label(value),
1642 payload.len(),
1643 hash,
1644 )
1645}
1646
1647const fn output_value_kind_label(value: &OutputValue) -> &'static str {
1648 match value {
1649 OutputValue::Account(_) => "account",
1650 OutputValue::Blob(_) => "blob",
1651 OutputValue::Bool(_) => "bool",
1652 OutputValue::Date(_) => "date",
1653 OutputValue::Decimal(_) => "decimal",
1654 OutputValue::Duration(_) => "duration",
1655 OutputValue::Enum(_) => "enum",
1656 OutputValue::Float32(_) => "float32",
1657 OutputValue::Float64(_) => "float64",
1658 OutputValue::Int64(_) => "int64",
1659 OutputValue::Int128(_) => "int128",
1660 OutputValue::IntBig(_) => "int_big",
1661 OutputValue::List(_) => "list",
1662 OutputValue::Map(_) => "map",
1663 OutputValue::Null => "null",
1664 OutputValue::Principal(_) => "principal",
1665 OutputValue::Subaccount(_) => "subaccount",
1666 OutputValue::Text(_) => "text",
1667 OutputValue::Timestamp(_) => "timestamp",
1668 OutputValue::Nat64(_) => "nat64",
1669 OutputValue::Nat128(_) => "nat128",
1670 OutputValue::NatBig(_) => "nat_big",
1671 OutputValue::Ulid(_) => "ulid",
1672 OutputValue::Unit => "unit",
1673 }
1674}
1675
1676fn short_default_payload_fingerprint(payload: &[u8]) -> String {
1677 let digest = Sha256::digest(payload);
1678 let mut out = String::with_capacity(16);
1679 for byte in &digest[..8] {
1680 let _ = write!(out, "{byte:02x}");
1681 }
1682 out
1683}
1684
1685#[cfg_attr(
1686 doc,
1687 doc = "Render one stable field-kind label from accepted persisted schema metadata."
1688)]
1689fn summarize_persisted_field_kind(
1690 kind: &AcceptedFieldKind,
1691 value_catalog: &AcceptedValueCatalogHandle,
1692) -> Result<String, InternalError> {
1693 let mut out = String::new();
1694 write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
1695
1696 Ok(out)
1697}
1698
1699fn write_persisted_field_kind_summary(
1702 out: &mut String,
1703 kind: &AcceptedFieldKind,
1704 value_catalog: &AcceptedValueCatalogHandle,
1705) -> Result<(), InternalError> {
1706 if let Some(name) = describe_kind_name(kind) {
1707 out.push_str(name);
1708 return Ok(());
1709 }
1710
1711 match kind {
1712 AcceptedFieldKind::Blob { max_len } => {
1713 write_length_bounded_field_kind_summary(out, "blob", *max_len);
1714 }
1715 AcceptedFieldKind::Decimal { scale } => {
1716 let _ = write!(out, "decimal(scale={scale})");
1717 }
1718 AcceptedFieldKind::IntBig { max_bytes } => {
1719 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
1720 }
1721 AcceptedFieldKind::Enum { type_id } => {
1722 let definition = value_catalog
1723 .enum_catalog()
1724 .enum_type(*type_id)
1725 .ok_or_else(InternalError::store_invariant)?;
1726 out.push_str("enum(");
1727 out.push_str(definition.path());
1728 out.push(')');
1729 }
1730 AcceptedFieldKind::Text { max_len } => {
1731 write_length_bounded_field_kind_summary(out, "text", *max_len);
1732 }
1733 AcceptedFieldKind::Relation {
1734 target_entity_name,
1735 key_kind,
1736 ..
1737 } => {
1738 out.push_str("relation(target=");
1739 out.push_str(target_entity_name);
1740 out.push_str(", key=");
1741 write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
1742 out.push(')');
1743 }
1744 AcceptedFieldKind::List(inner) => {
1745 out.push_str("list<");
1746 write_persisted_field_kind_summary(out, inner, value_catalog)?;
1747 out.push('>');
1748 }
1749 AcceptedFieldKind::Set(inner) => {
1750 out.push_str("set<");
1751 write_persisted_field_kind_summary(out, inner, value_catalog)?;
1752 out.push('>');
1753 }
1754 AcceptedFieldKind::Map { key, value } => {
1755 out.push_str("map<");
1756 write_persisted_field_kind_summary(out, key, value_catalog)?;
1757 out.push_str(", ");
1758 write_persisted_field_kind_summary(out, value, value_catalog)?;
1759 out.push('>');
1760 }
1761 AcceptedFieldKind::Composite { type_id } => {
1762 let composite_catalog = value_catalog.composite_catalog();
1763 let definition = composite_catalog
1764 .composite_type(*type_id)
1765 .ok_or_else(InternalError::store_invariant)?;
1766 out.push_str("composite(path=");
1767 out.push_str(definition.path());
1768 out.push_str(", codec=");
1769 write_composite_codec_summary(out, definition.codec());
1770 out.push_str(", shape=");
1771 write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
1772 out.push(')');
1773 }
1774 AcceptedFieldKind::Account
1775 | AcceptedFieldKind::Bool
1776 | AcceptedFieldKind::Date
1777 | AcceptedFieldKind::Duration
1778 | AcceptedFieldKind::Float32
1779 | AcceptedFieldKind::Float64
1780 | AcceptedFieldKind::Int8
1781 | AcceptedFieldKind::Int16
1782 | AcceptedFieldKind::Int32
1783 | AcceptedFieldKind::Int64
1784 | AcceptedFieldKind::Int128
1785 | AcceptedFieldKind::Principal
1786 | AcceptedFieldKind::Subaccount
1787 | AcceptedFieldKind::Timestamp
1788 | AcceptedFieldKind::Nat8
1789 | AcceptedFieldKind::Nat16
1790 | AcceptedFieldKind::Nat32
1791 | AcceptedFieldKind::Nat64
1792 | AcceptedFieldKind::Nat128
1793 | AcceptedFieldKind::Ulid
1794 | AcceptedFieldKind::Unit => return Err(InternalError::store_invariant()),
1795 AcceptedFieldKind::NatBig { max_bytes } => {
1796 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
1797 }
1798 }
1799
1800 Ok(())
1801}
1802
1803const fn describe_kind_name(kind: &AcceptedFieldKind) -> Option<&'static str> {
1804 Some(match kind {
1805 AcceptedFieldKind::Account => "account",
1806 AcceptedFieldKind::Bool => "bool",
1807 AcceptedFieldKind::Date => "date",
1808 AcceptedFieldKind::Duration => "duration",
1809 AcceptedFieldKind::Float32 => "float32",
1810 AcceptedFieldKind::Float64 => "float64",
1811 AcceptedFieldKind::Int8 => "int8",
1812 AcceptedFieldKind::Int16 => "int16",
1813 AcceptedFieldKind::Int32 => "int32",
1814 AcceptedFieldKind::Int64 => "int64",
1815 AcceptedFieldKind::Int128 => "int128",
1816 AcceptedFieldKind::Principal => "principal",
1817 AcceptedFieldKind::Subaccount => "subaccount",
1818 AcceptedFieldKind::Timestamp => "timestamp",
1819 AcceptedFieldKind::Nat8 => "nat8",
1820 AcceptedFieldKind::Nat16 => "nat16",
1821 AcceptedFieldKind::Nat32 => "nat32",
1822 AcceptedFieldKind::Nat64 => "nat64",
1823 AcceptedFieldKind::Nat128 => "nat128",
1824 AcceptedFieldKind::Ulid => "ulid",
1825 AcceptedFieldKind::Unit => "unit",
1826 AcceptedFieldKind::Blob { .. }
1827 | AcceptedFieldKind::Decimal { .. }
1828 | AcceptedFieldKind::Enum { .. }
1829 | AcceptedFieldKind::IntBig { .. }
1830 | AcceptedFieldKind::NatBig { .. }
1831 | AcceptedFieldKind::Text { .. }
1832 | AcceptedFieldKind::Relation { .. }
1833 | AcceptedFieldKind::List(_)
1834 | AcceptedFieldKind::Set(_)
1835 | AcceptedFieldKind::Map { .. }
1836 | AcceptedFieldKind::Composite { .. } => return None,
1837 })
1838}
1839
1840#[cfg(test)]
1845mod tests {
1846 use super::EntityIdentityDescription;
1847
1848 #[test]
1849 fn identity_description_reports_exact_remaining_capacity_and_exhaustion() {
1850 let available =
1851 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 254)
1852 .expect("in-domain Identity description should build");
1853 assert_eq!(available.minimum(), 1);
1854 assert_eq!(available.maximum(), 255);
1855 assert_eq!(available.high_water(), 254);
1856 assert_eq!(available.remaining(), 1);
1857 assert!(!available.exhausted());
1858
1859 let exhausted =
1860 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 255)
1861 .expect("exact-domain exhaustion should remain describable");
1862 assert_eq!(exhausted.remaining(), 0);
1863 assert!(exhausted.exhausted());
1864
1865 assert!(
1866 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 256).is_err(),
1867 "state beyond the accepted domain must not be described",
1868 );
1869 }
1870}