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, PersistedRelationEdgeSnapshot,
17 PersistedSchemaSnapshot, 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) struct AcceptedEntityDescriptionMetadata {
804 identity: Option<EntityIdentityDescription>,
805 entity_tag: u64,
806 accepted_schema_fingerprint_method: u8,
807 accepted_schema_fingerprint: [u8; 16],
808}
809
810impl AcceptedEntityDescriptionMetadata {
811 pub(in crate::db) const fn new(
813 identity: Option<EntityIdentityDescription>,
814 entity_tag: u64,
815 accepted_schema_fingerprint_method: u8,
816 accepted_schema_fingerprint: [u8; 16],
817 ) -> Self {
818 Self {
819 identity,
820 entity_tag,
821 accepted_schema_fingerprint_method,
822 accepted_schema_fingerprint,
823 }
824 }
825}
826
827pub(in crate::db) fn describe_accepted_entity_with_persisted_schema(
829 schema: &AcceptedSchemaSnapshot,
830 value_catalog: &AcceptedValueCatalogHandle,
831 validation_jobs: &[ConstraintValidationJob],
832 metadata: AcceptedEntityDescriptionMetadata,
833 resolve_relation_target: impl Fn(&str) -> Result<(String, String), InternalError>,
834) -> Result<EntitySchemaDescription, InternalError> {
835 describe_entity_with_persisted_schema(
836 schema,
837 value_catalog,
838 validation_jobs,
839 metadata,
840 &resolve_relation_target,
841 )
842}
843
844fn describe_entity_with_persisted_schema(
845 schema: &AcceptedSchemaSnapshot,
846 value_catalog: &AcceptedValueCatalogHandle,
847 validation_jobs: &[ConstraintValidationJob],
848 metadata: AcceptedEntityDescriptionMetadata,
849 resolve_relation_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
850) -> Result<EntitySchemaDescription, InternalError> {
851 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
852 let fields = describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)?;
853 let primary_key_fields = schema.primary_key_field_names();
854 if primary_key_fields.is_empty() {
855 return Err(InternalError::store_invariant());
856 }
857 let primary_key_fields = primary_key_fields
858 .into_iter()
859 .map(str::to_string)
860 .collect::<Vec<_>>();
861 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
862
863 Ok(describe_entity_model_from_description_rows(
864 schema.entity_path(),
865 schema.entity_name(),
866 metadata.entity_tag,
867 metadata.accepted_schema_fingerprint_method,
868 metadata.accepted_schema_fingerprint,
869 primary_key.as_str(),
870 primary_key_fields,
871 fields,
872 describe_entity_indexes_with_persisted_schema(schema),
873 describe_entity_relations_with_persisted_schema(schema, resolve_relation_target)?,
874 describe_entity_constraints_with_persisted_schema(schema, value_catalog, validation_jobs)?,
875 row_layout.current_layout_version().get(),
876 row_layout.history_floor().get(),
877 )
878 .with_identity(metadata.identity))
879}
880
881#[expect(
886 clippy::too_many_arguments,
887 reason = "one final schema DTO assembly keeps every already-owned section explicit"
888)]
889fn describe_entity_model_from_description_rows(
890 entity_path: &str,
891 entity_name: &str,
892 entity_tag: u64,
893 accepted_schema_fingerprint_method: u8,
894 accepted_schema_fingerprint: [u8; 16],
895 primary_key: &str,
896 primary_key_fields: Vec<String>,
897 fields: Vec<EntityFieldDescription>,
898 indexes: Vec<EntityIndexDescription>,
899 relations: Vec<EntityRelationDescription>,
900 constraints: Vec<EntityConstraintDescription>,
901 row_layout_current: u32,
902 row_layout_history_floor: u32,
903) -> EntitySchemaDescription {
904 EntitySchemaDescription::new(
905 entity_path.to_string(),
906 entity_name.to_string(),
907 entity_tag,
908 accepted_schema_fingerprint_method,
909 accepted_schema_fingerprint,
910 primary_key.to_string(),
911 primary_key_fields,
912 fields,
913 indexes,
914 relations,
915 constraints,
916 row_layout_current,
917 row_layout_history_floor,
918 )
919}
920
921fn describe_entity_constraints_with_persisted_schema(
922 schema: &AcceptedSchemaSnapshot,
923 value_catalog: &AcceptedValueCatalogHandle,
924 validation_jobs: &[ConstraintValidationJob],
925) -> Result<Vec<EntityConstraintDescription>, InternalError> {
926 let snapshot = schema.persisted_snapshot();
927 let mut descriptions = snapshot
928 .constraints()
929 .iter()
930 .map(|constraint| describe_accepted_constraint(snapshot, value_catalog, constraint))
931 .collect::<Result<Vec<_>, InternalError>>()?;
932 descriptions.extend(
933 snapshot
934 .constraint_activations()
935 .iter()
936 .map(|activation| {
937 let job = validation_jobs
938 .iter()
939 .find(|job| job.constraint_id() == activation.id());
940 describe_constraint_activation(snapshot, value_catalog, activation, job)
941 })
942 .collect::<Result<Vec<_>, InternalError>>()?,
943 );
944 if validation_jobs.iter().any(|job| {
945 !snapshot
946 .constraint_activations()
947 .iter()
948 .any(|activation| activation.id() == job.constraint_id())
949 }) {
950 return Err(InternalError::store_invariant());
951 }
952 descriptions.sort_unstable_by_key(|description| {
953 (
954 description.id(),
955 description.validation_state() != "validated",
956 )
957 });
958 Ok(descriptions)
959}
960
961fn describe_accepted_constraint(
962 snapshot: &PersistedSchemaSnapshot,
963 value_catalog: &AcceptedValueCatalogHandle,
964 constraint: &crate::db::schema::AcceptedConstraintSnapshot,
965) -> Result<EntityConstraintDescription, InternalError> {
966 let mut description = accepted_constraint_description(
967 constraint.id().get(),
968 constraint.name(),
969 constraint.origin(),
970 );
971 match constraint.kind() {
972 AcceptedConstraintKind::PrimaryKey => {
973 description.kind = "primary_key".to_string();
974 description.fields = snapshot
975 .primary_key_field_ids()
976 .iter()
977 .map(|field_id| accepted_field_name(snapshot, *field_id))
978 .collect::<Result<Vec<_>, _>>()?;
979 description.semantics = "primary_key_v1".to_string();
980 }
981 AcceptedConstraintKind::NotNull { field_id } => {
982 description.kind = "not_null".to_string();
983 description.field_id = Some(field_id.get());
984 description.fields = vec![accepted_field_name(snapshot, *field_id)?];
985 description.semantics = "not_null_v1".to_string();
986 }
987 AcceptedConstraintKind::Unique { index_id } => {
988 let index = snapshot
989 .indexes()
990 .iter()
991 .find(|index| index.schema_id() == *index_id)
992 .ok_or_else(InternalError::store_invariant)?;
993 description.kind = "unique".to_string();
994 description.index_id = Some(index_id.get());
995 description.fields = describe_persisted_index_fields(index.key());
996 description.index = Some(index.name().to_string());
997 description.semantics = "unique_index_v1".to_string();
998 }
999 AcceptedConstraintKind::Relation { relation_id } => {
1000 let relation = snapshot
1001 .relations()
1002 .iter()
1003 .find(|relation| relation.id() == *relation_id)
1004 .ok_or_else(InternalError::store_invariant)?;
1005 description.kind = "relation".to_string();
1006 description.relation_id = Some(relation_id.get());
1007 description.fields = relation
1008 .local_field_ids()
1009 .iter()
1010 .map(|field_id| accepted_field_name(snapshot, *field_id))
1011 .collect::<Result<Vec<_>, _>>()?;
1012 description.relation = Some(relation.name().to_string());
1013 description.target_entity = Some(relation.target_path().to_string());
1014 description.action = Some("restrict".to_string());
1015 description.semantics = "relation_pk_restrict_v1".to_string();
1016 }
1017 AcceptedConstraintKind::Check { expression } => {
1018 description.kind = "check".to_string();
1019 description.fields = expression
1020 .dependencies()
1021 .into_iter()
1022 .map(|field_id| accepted_field_name(snapshot, field_id))
1023 .collect::<Result<Vec<_>, _>>()?;
1024 description.semantics = "check_expr_v1".to_string();
1025 description.check_sql = Some(render_accepted_check_expr_sql(
1026 expression,
1027 snapshot,
1028 value_catalog,
1029 )?);
1030 }
1031 AcceptedConstraintKind::TargetedRule { target, operation } => {
1032 description.kind = "targeted_rule".to_string();
1033 description.field_id = Some(target.root_field_id().get());
1034 description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1035 description.semantics = match operation.as_ref() {
1036 crate::db::schema::AcceptedRuleOperation::LengthRangeInclusive { .. } => {
1037 "targeted_length_range_v1"
1038 }
1039 crate::db::schema::AcceptedRuleOperation::MultipleOf { .. } => {
1040 "targeted_multiple_of_v1"
1041 }
1042 crate::db::schema::AcceptedRuleOperation::NumericMaximumInclusive { .. } => {
1043 "targeted_numeric_maximum_v1"
1044 }
1045 crate::db::schema::AcceptedRuleOperation::NumericMinimumInclusive { .. } => {
1046 "targeted_numeric_minimum_v1"
1047 }
1048 crate::db::schema::AcceptedRuleOperation::NumericRangeInclusive { .. } => {
1049 "targeted_numeric_range_v1"
1050 }
1051 }
1052 .to_string();
1053 }
1054 }
1055 Ok(description)
1056}
1057
1058fn describe_constraint_activation(
1059 snapshot: &PersistedSchemaSnapshot,
1060 value_catalog: &AcceptedValueCatalogHandle,
1061 activation: &ConstraintActivationSnapshot,
1062 validation_job: Option<&ConstraintValidationJob>,
1063) -> Result<EntityConstraintDescription, InternalError> {
1064 let mut description = accepted_constraint_description(
1065 activation.id().get(),
1066 activation.name(),
1067 activation.origin(),
1068 );
1069 match activation.state() {
1070 ConstraintActivationState::EnforcingNewWrites if validation_job.is_none() => {
1071 description.validation_state = "enforcing_new_writes".to_string();
1072 }
1073 ConstraintActivationState::Validating => {
1074 let job = validation_job.ok_or_else(InternalError::store_invariant)?;
1075 job.validate(Some(activation))?;
1076 description.validation_state = "validating".to_string();
1077 description.validation_progress =
1078 Some(ConstraintValidationProgressDescription::from_job(job));
1079 }
1080 ConstraintActivationState::EnforcingNewWrites => {
1081 return Err(InternalError::store_invariant());
1082 }
1083 }
1084 match activation.kind() {
1085 ConstraintActivationKind::NotNull { field_id } => {
1086 description.kind = "not_null".to_string();
1087 description.field_id = Some(field_id.get());
1088 description.fields = vec![accepted_field_name(snapshot, *field_id)?];
1089 description.semantics = "not_null_v1".to_string();
1090 }
1091 ConstraintActivationKind::Unique { index_id } => {
1092 let index = snapshot
1093 .candidate_indexes()
1094 .iter()
1095 .find(|index| index.schema_id() == *index_id)
1096 .ok_or_else(InternalError::store_invariant)?;
1097 description.kind = "unique".to_string();
1098 description.index_id = Some(index_id.get());
1099 description.fields = describe_persisted_index_fields(index.key());
1100 description.index = Some(index.name().to_string());
1101 description.semantics = "unique_index_v1".to_string();
1102 }
1103 ConstraintActivationKind::Relation { relation_id } => {
1104 let relation = snapshot
1105 .candidate_relations()
1106 .iter()
1107 .find(|relation| relation.id() == *relation_id)
1108 .ok_or_else(InternalError::store_invariant)?;
1109 description.kind = "relation".to_string();
1110 description.relation_id = Some(relation_id.get());
1111 description.fields = relation
1112 .local_field_ids()
1113 .iter()
1114 .map(|field_id| accepted_field_name(snapshot, *field_id))
1115 .collect::<Result<Vec<_>, _>>()?;
1116 description.relation = Some(relation.name().to_string());
1117 description.target_entity = Some(relation.target_path().to_string());
1118 description.action = Some("restrict".to_string());
1119 description.semantics = "relation_pk_restrict_v1".to_string();
1120 }
1121 ConstraintActivationKind::Check { expression } => {
1122 description.kind = "check".to_string();
1123 description.fields = expression
1124 .dependencies()
1125 .into_iter()
1126 .map(|field_id| accepted_field_name(snapshot, field_id))
1127 .collect::<Result<Vec<_>, _>>()?;
1128 description.semantics = "check_expr_v1".to_string();
1129 description.check_sql = Some(render_accepted_check_expr_sql(
1130 expression,
1131 snapshot,
1132 value_catalog,
1133 )?);
1134 }
1135 ConstraintActivationKind::TargetedRule { target, operation } => {
1136 description.kind = "targeted_rule".to_string();
1137 description.field_id = Some(target.root_field_id().get());
1138 description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1139 description.semantics = match operation.as_ref() {
1140 crate::db::schema::AcceptedRuleOperation::LengthRangeInclusive { .. } => {
1141 "targeted_length_range_v1"
1142 }
1143 crate::db::schema::AcceptedRuleOperation::MultipleOf { .. } => {
1144 "targeted_multiple_of_v1"
1145 }
1146 crate::db::schema::AcceptedRuleOperation::NumericMaximumInclusive { .. } => {
1147 "targeted_numeric_maximum_v1"
1148 }
1149 crate::db::schema::AcceptedRuleOperation::NumericMinimumInclusive { .. } => {
1150 "targeted_numeric_minimum_v1"
1151 }
1152 crate::db::schema::AcceptedRuleOperation::NumericRangeInclusive { .. } => {
1153 "targeted_numeric_range_v1"
1154 }
1155 }
1156 .to_string();
1157 }
1158 }
1159 Ok(description)
1160}
1161
1162fn accepted_constraint_description(
1163 id: u32,
1164 name: &str,
1165 origin: ConstraintOrigin,
1166) -> EntityConstraintDescription {
1167 EntityConstraintDescription {
1168 id,
1169 name: name.to_string(),
1170 kind: String::new(),
1171 origin: accepted_constraint_origin_label(origin).to_string(),
1172 validation_state: "validated".to_string(),
1173 validation_progress: None,
1174 field_id: None,
1175 index_id: None,
1176 relation_id: None,
1177 fields: Vec::new(),
1178 index: None,
1179 relation: None,
1180 target_entity: None,
1181 action: None,
1182 semantics: String::new(),
1183 check_sql: None,
1184 }
1185}
1186
1187const fn accepted_constraint_origin_label(origin: ConstraintOrigin) -> &'static str {
1188 match origin {
1189 ConstraintOrigin::Generated => "generated",
1190 ConstraintOrigin::SqlDdl => "sql_ddl",
1191 }
1192}
1193
1194fn accepted_field_name(
1195 snapshot: &crate::db::schema::PersistedSchemaSnapshot,
1196 field_id: FieldId,
1197) -> Result<String, InternalError> {
1198 snapshot
1199 .fields()
1200 .iter()
1201 .find(|field| field.id() == field_id)
1202 .map(|field| field.name().to_string())
1203 .ok_or_else(InternalError::store_invariant)
1204}
1205
1206fn render_primary_key_fields(fields: &[String]) -> String {
1207 fields.join(", ")
1208}
1209
1210fn describe_entity_indexes_with_persisted_schema(
1211 schema: &AcceptedSchemaSnapshot,
1212) -> Vec<EntityIndexDescription> {
1213 schema
1214 .persisted_snapshot()
1215 .indexes()
1216 .iter()
1217 .map(|index| {
1218 EntityIndexDescription::new(
1219 index.name().to_string(),
1220 index.unique(),
1221 describe_persisted_index_fields(index.key()),
1222 if index.generated() {
1223 "generated".to_string()
1224 } else {
1225 "ddl".to_string()
1226 },
1227 )
1228 })
1229 .collect()
1230}
1231
1232fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
1233 match key {
1234 PersistedIndexKeySnapshot::FieldPath(paths) => paths
1235 .iter()
1236 .map(|field_path| field_path.path().join("."))
1237 .collect(),
1238 PersistedIndexKeySnapshot::Items(items) => items
1239 .iter()
1240 .map(|item| match item {
1241 PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
1242 PersistedIndexKeyItemSnapshot::Expression(expression) => {
1243 expression.canonical_text().to_string()
1244 }
1245 })
1246 .collect(),
1247 }
1248}
1249
1250#[cfg_attr(
1251 doc,
1252 doc = "Build field descriptors using accepted persisted schema slot metadata."
1253)]
1254#[cfg(any(test, feature = "sql"))]
1255pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1256 schema: &AcceptedSchemaSnapshot,
1257 value_catalog: &AcceptedValueCatalogHandle,
1258) -> Result<Vec<EntityFieldDescription>, InternalError> {
1259 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1260 describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1261}
1262
1263fn describe_entity_fields_with_runtime_contract(
1264 schema: &AcceptedSchemaSnapshot,
1265 row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1266 value_catalog: &AcceptedValueCatalogHandle,
1267) -> Result<Vec<EntityFieldDescription>, InternalError> {
1268 let snapshot = schema.persisted_snapshot();
1269 if snapshot.fields().len() != row_layout.fields().len() {
1270 return Err(InternalError::store_invariant());
1271 }
1272 let mut fields = Vec::with_capacity(snapshot.fields().len());
1273
1274 for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1277 if field.id() != runtime_field.field_id() {
1278 return Err(InternalError::store_invariant());
1279 }
1280 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1281 let slot = Some(runtime_field.slot().get());
1282 let metadata = DescribeFieldMetadata::new(
1283 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1284 field.nullable(),
1285 field_type_from_persisted_kind(field.kind()).is_queryable(),
1286 field_origin_label(field.generated()),
1287 );
1288 let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1289
1290 push_described_field_row(
1291 &mut fields,
1292 field.name(),
1293 slot,
1294 primary_key,
1295 None,
1296 metadata,
1297 temporal,
1298 );
1299
1300 if !field.nested_leaves().is_empty() {
1301 describe_persisted_nested_leaves(
1302 &mut fields,
1303 field.nested_leaves(),
1304 field_origin_label(field.generated()),
1305 value_catalog,
1306 )?;
1307 }
1308 }
1309
1310 Ok(fields)
1311}
1312
1313struct DescribeFieldMetadata {
1320 kind: String,
1321 nullable: bool,
1322 queryable: bool,
1323 origin: String,
1324}
1325
1326impl DescribeFieldMetadata {
1327 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1329 Self {
1330 kind,
1331 nullable,
1332 queryable,
1333 origin,
1334 }
1335 }
1336}
1337
1338fn push_described_field_row(
1341 fields: &mut Vec<EntityFieldDescription>,
1342 name: &str,
1343 slot: Option<u16>,
1344 primary_key: bool,
1345 tree_prefix: Option<&'static str>,
1346 metadata: DescribeFieldMetadata,
1347 temporal: EntityFieldTemporalFacts,
1348) {
1349 let display_name = if let Some(prefix) = tree_prefix {
1352 format!("{prefix}{name}")
1353 } else {
1354 name.to_string()
1355 };
1356
1357 fields.push(EntityFieldDescription::new_with_temporal_facts(
1358 display_name,
1359 slot,
1360 primary_key,
1361 metadata,
1362 temporal,
1363 ));
1364}
1365
1366fn describe_persisted_nested_leaves(
1369 fields: &mut Vec<EntityFieldDescription>,
1370 nested_leaves: &[PersistedNestedLeafSnapshot],
1371 origin: String,
1372 value_catalog: &AcceptedValueCatalogHandle,
1373) -> Result<(), InternalError> {
1374 for (index, leaf) in nested_leaves.iter().enumerate() {
1375 let prefix = if index + 1 == nested_leaves.len() {
1376 "└─ "
1377 } else {
1378 "├─ "
1379 };
1380 let name = leaf.path().last().map_or("", String::as_str);
1381 let metadata = DescribeFieldMetadata::new(
1382 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1383 leaf.nullable(),
1384 field_type_from_persisted_kind(leaf.kind()).is_queryable(),
1385 origin.clone(),
1386 );
1387
1388 push_described_field_row(
1389 fields,
1390 name,
1391 None,
1392 false,
1393 Some(prefix),
1394 metadata,
1395 EntityFieldTemporalFacts::nested(),
1396 );
1397 }
1398
1399 Ok(())
1400}
1401
1402fn field_origin_label(generated: bool) -> String {
1403 if generated {
1404 "generated".to_string()
1405 } else {
1406 "ddl".to_string()
1407 }
1408}
1409
1410fn describe_entity_relations_with_persisted_schema(
1411 schema: &AcceptedSchemaSnapshot,
1412 resolve_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
1413) -> Result<Vec<EntityRelationDescription>, InternalError> {
1414 let snapshot = schema.persisted_snapshot();
1415 snapshot
1416 .relations()
1417 .iter()
1418 .map(|relation| {
1419 let local_fields = relation
1420 .local_field_ids()
1421 .iter()
1422 .map(|field_id| accepted_field_name(snapshot, *field_id))
1423 .collect::<Result<Vec<_>, _>>()?;
1424 let (target_entity_name, target_store_path) = resolve_target(relation.target_path())?;
1425
1426 Ok(EntityRelationDescription::new(
1427 render_primary_key_fields(local_fields.as_slice()),
1428 relation.target_path().to_string(),
1429 target_entity_name,
1430 target_store_path,
1431 persisted_relation_cardinality(snapshot, relation)?,
1432 ))
1433 })
1434 .collect()
1435}
1436
1437fn persisted_relation_cardinality(
1438 snapshot: &PersistedSchemaSnapshot,
1439 relation: &PersistedRelationEdgeSnapshot,
1440) -> Result<EntityRelationCardinality, InternalError> {
1441 let [field_id] = relation.local_field_ids() else {
1442 return Ok(EntityRelationCardinality::Single);
1443 };
1444 let field = snapshot
1445 .fields()
1446 .iter()
1447 .find(|field| field.id() == *field_id)
1448 .ok_or_else(InternalError::store_invariant)?;
1449
1450 Ok(match field.kind() {
1451 AcceptedFieldKind::List(_) => EntityRelationCardinality::List,
1452 AcceptedFieldKind::Set(_) => EntityRelationCardinality::Set,
1453 _ => EntityRelationCardinality::Single,
1454 })
1455}
1456
1457fn write_accepted_composite_shape_summary(
1458 out: &mut String,
1459 shape: &AcceptedCompositeShape,
1460 value_catalog: &AcceptedValueCatalogHandle,
1461) -> Result<(), InternalError> {
1462 match shape {
1463 AcceptedCompositeShape::Record(fields) => {
1464 out.push_str("record{");
1465 for (index, field) in fields.iter().enumerate() {
1466 if index > 0 {
1467 out.push_str(", ");
1468 }
1469 out.push_str(field.name());
1470 out.push(':');
1471 write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1472 }
1473 out.push('}');
1474 }
1475 AcceptedCompositeShape::Tuple(elements) => {
1476 out.push_str("tuple<");
1477 for (index, element) in elements.iter().enumerate() {
1478 if index > 0 {
1479 out.push_str(", ");
1480 }
1481 write_accepted_composite_element_summary(out, element, value_catalog)?;
1482 }
1483 out.push('>');
1484 }
1485 AcceptedCompositeShape::Newtype(inner) => {
1486 out.push_str("newtype<");
1487 write_accepted_composite_element_summary(out, inner, value_catalog)?;
1488 out.push('>');
1489 }
1490 }
1491
1492 Ok(())
1493}
1494
1495fn write_accepted_composite_element_summary(
1496 out: &mut String,
1497 element: &AcceptedCompositeElement,
1498 value_catalog: &AcceptedValueCatalogHandle,
1499) -> Result<(), InternalError> {
1500 write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1501 write_composite_nullability_summary(out, element.nullable());
1502 Ok(())
1503}
1504
1505fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1506 match codec {
1507 CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1508 }
1509}
1510
1511fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1512 if nullable {
1513 out.push('?');
1514 }
1515}
1516
1517fn write_length_bounded_field_kind_summary(
1521 out: &mut String,
1522 kind_name: &str,
1523 max_len: Option<u32>,
1524) {
1525 out.push_str(kind_name);
1526 if let Some(max_len) = max_len {
1527 out.push_str("(max_len=");
1528 out.push_str(&max_len.to_string());
1529 out.push(')');
1530 } else {
1531 out.push_str("(unbounded)");
1532 }
1533}
1534
1535fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1536 out.push_str(kind_name);
1537 out.push_str("(max_bytes=");
1538 out.push_str(&max_bytes.to_string());
1539 out.push(')');
1540}
1541
1542struct RenderedTemporalPayload {
1550 value: String,
1551 bytes: u32,
1552 hash: String,
1553}
1554
1555fn accepted_field_temporal_facts(
1556 field: &AcceptedRowLayoutRuntimeField<'_>,
1557 value_catalog: &AcceptedValueCatalogHandle,
1558) -> Result<EntityFieldTemporalFacts, InternalError> {
1559 let write_policy = field.write_policy();
1560 let insert_omission = if write_policy.insert_generation().is_some() {
1561 "generated"
1562 } else if write_policy.write_management().is_some() {
1563 "managed"
1564 } else {
1565 match field.insert_omission_policy() {
1566 AcceptedInsertOmissionPolicy::NullIfMissing => "null",
1567 AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
1568 AcceptedInsertOmissionPolicy::Required => "required",
1569 }
1570 };
1571 let insert_default = field
1572 .insert_default()
1573 .slot_payload()
1574 .map(|payload| accepted_payload_facts(field, value_catalog, payload))
1575 .transpose()?;
1576 let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
1577 Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
1578 None => (None, None, None),
1579 };
1580 let (historical_fill, historical_fill_bytes, historical_fill_hash) =
1581 match field.historical_fill() {
1582 SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
1583 SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
1584 SchemaHistoricalFill::SlotPayload(payload) => {
1585 let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
1586 (
1587 Some(rendered.value),
1588 Some(rendered.bytes),
1589 Some(rendered.hash),
1590 )
1591 }
1592 };
1593
1594 Ok(EntityFieldTemporalFacts {
1595 insert_omission: Some(insert_omission.to_string()),
1596 insert_default,
1597 insert_default_bytes,
1598 insert_default_hash,
1599 introduced_in_layout: Some(field.introduced_in_layout().get()),
1600 historical_fill,
1601 historical_fill_bytes,
1602 historical_fill_hash,
1603 })
1604}
1605
1606fn accepted_payload_facts(
1607 field: &AcceptedRowLayoutRuntimeField<'_>,
1608 value_catalog: &AcceptedValueCatalogHandle,
1609 payload: &[u8],
1610) -> Result<RenderedTemporalPayload, InternalError> {
1611 let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
1612 .map_err(|_| InternalError::store_invariant())?;
1613 let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
1614 let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.value())
1615 .map_err(|_| InternalError::store_invariant())?;
1616 let hash = short_default_payload_fingerprint(payload);
1617 let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
1618 let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
1619
1620 Ok(RenderedTemporalPayload {
1621 value: rendered,
1622 bytes,
1623 hash,
1624 })
1625}
1626
1627fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
1628 let rendered = match value {
1629 OutputValue::Text(value) => format!("'{}'", value.escape_default()),
1630 _ => render_output_value_text(value),
1631 };
1632 if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
1633 return rendered;
1634 }
1635
1636 format!(
1637 "{}(bytes={}, sha256={})",
1638 output_value_kind_label(value),
1639 payload.len(),
1640 hash,
1641 )
1642}
1643
1644const fn output_value_kind_label(value: &OutputValue) -> &'static str {
1645 match value {
1646 OutputValue::Account(_) => "account",
1647 OutputValue::Blob(_) => "blob",
1648 OutputValue::Bool(_) => "bool",
1649 OutputValue::Date(_) => "date",
1650 OutputValue::Decimal(_) => "decimal",
1651 OutputValue::Duration(_) => "duration",
1652 OutputValue::Enum(_) => "enum",
1653 OutputValue::Float32(_) => "float32",
1654 OutputValue::Float64(_) => "float64",
1655 OutputValue::Int64(_) => "int64",
1656 OutputValue::Int128(_) => "int128",
1657 OutputValue::IntBig(_) => "int_big",
1658 OutputValue::List(_) => "list",
1659 OutputValue::Map(_) => "map",
1660 OutputValue::Null => "null",
1661 OutputValue::Principal(_) => "principal",
1662 OutputValue::Subaccount(_) => "subaccount",
1663 OutputValue::Text(_) => "text",
1664 OutputValue::Timestamp(_) => "timestamp",
1665 OutputValue::Nat64(_) => "nat64",
1666 OutputValue::Nat128(_) => "nat128",
1667 OutputValue::NatBig(_) => "nat_big",
1668 OutputValue::Ulid(_) => "ulid",
1669 OutputValue::Unit => "unit",
1670 }
1671}
1672
1673fn short_default_payload_fingerprint(payload: &[u8]) -> String {
1674 let digest = Sha256::digest(payload);
1675 let mut out = String::with_capacity(16);
1676 for byte in &digest[..8] {
1677 let _ = write!(out, "{byte:02x}");
1678 }
1679 out
1680}
1681
1682#[cfg_attr(
1683 doc,
1684 doc = "Render one stable field-kind label from accepted persisted schema metadata."
1685)]
1686fn summarize_persisted_field_kind(
1687 kind: &AcceptedFieldKind,
1688 value_catalog: &AcceptedValueCatalogHandle,
1689) -> Result<String, InternalError> {
1690 let mut out = String::new();
1691 write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
1692
1693 Ok(out)
1694}
1695
1696fn write_persisted_field_kind_summary(
1699 out: &mut String,
1700 kind: &AcceptedFieldKind,
1701 value_catalog: &AcceptedValueCatalogHandle,
1702) -> Result<(), InternalError> {
1703 if let Some(name) = describe_kind_name(kind) {
1704 out.push_str(name);
1705 return Ok(());
1706 }
1707
1708 match kind {
1709 AcceptedFieldKind::Blob { max_len } => {
1710 write_length_bounded_field_kind_summary(out, "blob", *max_len);
1711 }
1712 AcceptedFieldKind::Decimal { scale } => {
1713 let _ = write!(out, "decimal(scale={scale})");
1714 }
1715 AcceptedFieldKind::IntBig { max_bytes } => {
1716 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
1717 }
1718 AcceptedFieldKind::Enum { type_id } => {
1719 let definition = value_catalog
1720 .enum_catalog()
1721 .enum_type(*type_id)
1722 .ok_or_else(InternalError::store_invariant)?;
1723 out.push_str("enum(");
1724 out.push_str(definition.path());
1725 out.push(')');
1726 }
1727 AcceptedFieldKind::Text { max_len } => {
1728 write_length_bounded_field_kind_summary(out, "text", *max_len);
1729 }
1730 AcceptedFieldKind::Relation {
1731 target_entity_name,
1732 key_kind,
1733 ..
1734 } => {
1735 out.push_str("relation(target=");
1736 out.push_str(target_entity_name);
1737 out.push_str(", key=");
1738 write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
1739 out.push(')');
1740 }
1741 AcceptedFieldKind::List(inner) => {
1742 out.push_str("list<");
1743 write_persisted_field_kind_summary(out, inner, value_catalog)?;
1744 out.push('>');
1745 }
1746 AcceptedFieldKind::Set(inner) => {
1747 out.push_str("set<");
1748 write_persisted_field_kind_summary(out, inner, value_catalog)?;
1749 out.push('>');
1750 }
1751 AcceptedFieldKind::Map { key, value } => {
1752 out.push_str("map<");
1753 write_persisted_field_kind_summary(out, key, value_catalog)?;
1754 out.push_str(", ");
1755 write_persisted_field_kind_summary(out, value, value_catalog)?;
1756 out.push('>');
1757 }
1758 AcceptedFieldKind::Composite { type_id } => {
1759 let composite_catalog = value_catalog.composite_catalog();
1760 let definition = composite_catalog
1761 .composite_type(*type_id)
1762 .ok_or_else(InternalError::store_invariant)?;
1763 out.push_str("composite(path=");
1764 out.push_str(definition.path());
1765 out.push_str(", codec=");
1766 write_composite_codec_summary(out, definition.codec());
1767 out.push_str(", shape=");
1768 write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
1769 out.push(')');
1770 }
1771 AcceptedFieldKind::Account
1772 | AcceptedFieldKind::Bool
1773 | AcceptedFieldKind::Date
1774 | AcceptedFieldKind::Duration
1775 | AcceptedFieldKind::Float32
1776 | AcceptedFieldKind::Float64
1777 | AcceptedFieldKind::Int8
1778 | AcceptedFieldKind::Int16
1779 | AcceptedFieldKind::Int32
1780 | AcceptedFieldKind::Int64
1781 | AcceptedFieldKind::Int128
1782 | AcceptedFieldKind::Principal
1783 | AcceptedFieldKind::Subaccount
1784 | AcceptedFieldKind::Timestamp
1785 | AcceptedFieldKind::Nat8
1786 | AcceptedFieldKind::Nat16
1787 | AcceptedFieldKind::Nat32
1788 | AcceptedFieldKind::Nat64
1789 | AcceptedFieldKind::Nat128
1790 | AcceptedFieldKind::Ulid
1791 | AcceptedFieldKind::Unit => return Err(InternalError::store_invariant()),
1792 AcceptedFieldKind::NatBig { max_bytes } => {
1793 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
1794 }
1795 }
1796
1797 Ok(())
1798}
1799
1800const fn describe_kind_name(kind: &AcceptedFieldKind) -> Option<&'static str> {
1801 Some(match kind {
1802 AcceptedFieldKind::Account => "account",
1803 AcceptedFieldKind::Bool => "bool",
1804 AcceptedFieldKind::Date => "date",
1805 AcceptedFieldKind::Duration => "duration",
1806 AcceptedFieldKind::Float32 => "float32",
1807 AcceptedFieldKind::Float64 => "float64",
1808 AcceptedFieldKind::Int8 => "int8",
1809 AcceptedFieldKind::Int16 => "int16",
1810 AcceptedFieldKind::Int32 => "int32",
1811 AcceptedFieldKind::Int64 => "int64",
1812 AcceptedFieldKind::Int128 => "int128",
1813 AcceptedFieldKind::Principal => "principal",
1814 AcceptedFieldKind::Subaccount => "subaccount",
1815 AcceptedFieldKind::Timestamp => "timestamp",
1816 AcceptedFieldKind::Nat8 => "nat8",
1817 AcceptedFieldKind::Nat16 => "nat16",
1818 AcceptedFieldKind::Nat32 => "nat32",
1819 AcceptedFieldKind::Nat64 => "nat64",
1820 AcceptedFieldKind::Nat128 => "nat128",
1821 AcceptedFieldKind::Ulid => "ulid",
1822 AcceptedFieldKind::Unit => "unit",
1823 AcceptedFieldKind::Blob { .. }
1824 | AcceptedFieldKind::Decimal { .. }
1825 | AcceptedFieldKind::Enum { .. }
1826 | AcceptedFieldKind::IntBig { .. }
1827 | AcceptedFieldKind::NatBig { .. }
1828 | AcceptedFieldKind::Text { .. }
1829 | AcceptedFieldKind::Relation { .. }
1830 | AcceptedFieldKind::List(_)
1831 | AcceptedFieldKind::Set(_)
1832 | AcceptedFieldKind::Map { .. }
1833 | AcceptedFieldKind::Composite { .. } => return None,
1834 })
1835}
1836
1837#[cfg(test)]
1842mod tests {
1843 use super::EntityIdentityDescription;
1844
1845 #[test]
1846 fn identity_description_reports_exact_remaining_capacity_and_exhaustion() {
1847 let available =
1848 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 254)
1849 .expect("in-domain Identity description should build");
1850 assert_eq!(available.minimum(), 1);
1851 assert_eq!(available.maximum(), 255);
1852 assert_eq!(available.high_water(), 254);
1853 assert_eq!(available.remaining(), 1);
1854 assert!(!available.exhausted());
1855
1856 let exhausted =
1857 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 255)
1858 .expect("exact-domain exhaustion should remain describable");
1859 assert_eq!(exhausted.remaining(), 0);
1860 assert!(exhausted.exhausted());
1861
1862 assert!(
1863 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 256).is_err(),
1864 "state beyond the accepted domain must not be described",
1865 );
1866 }
1867}