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