Skip to main content

icydb_core/db/schema/
describe.rs

1//! Module: db::schema::describe
2//! Responsibility: deterministic entity-schema introspection DTOs for runtime consumers.
3//! Does not own: query planning, execution routing, or relation enforcement semantics.
4//! Boundary: projects accepted schema metadata into stable describe surfaces.
5
6use 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    /// Construct one entity schema description payload.
60    #[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    /// Borrow the entity module path.
99    #[must_use]
100    pub const fn entity_path(&self) -> &str {
101        self.entity_path.as_str()
102    }
103
104    /// Borrow the entity display name.
105    #[must_use]
106    pub const fn entity_name(&self) -> &str {
107        self.entity_name.as_str()
108    }
109
110    /// Return the accepted durable entity identity used by diagnostic facts.
111    #[must_use]
112    pub const fn entity_tag(&self) -> u64 {
113        self.entity_tag
114    }
115
116    /// Return the accepted schema-fingerprint method used by diagnostic facts.
117    #[must_use]
118    pub const fn accepted_schema_fingerprint_method(&self) -> u8 {
119        self.accepted_schema_fingerprint_method
120    }
121
122    /// Return the exact accepted entity-schema fingerprint.
123    #[must_use]
124    pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
125        self.accepted_schema_fingerprint
126    }
127
128    /// Borrow the rendered primary-key field list.
129    #[must_use]
130    pub const fn primary_key(&self) -> &str {
131        self.primary_key.as_str()
132    }
133
134    /// Borrow ordered primary-key field names.
135    #[must_use]
136    pub const fn primary_key_fields(&self) -> &[String] {
137        self.primary_key_fields.as_slice()
138    }
139
140    /// Borrow the accepted Identity policy and lifetime allocation state.
141    #[must_use]
142    pub fn identity(&self) -> Option<&EntityIdentityDescription> {
143        self.identity.as_deref()
144    }
145
146    /// Borrow field description entries.
147    #[must_use]
148    pub const fn fields(&self) -> &[EntityFieldDescription] {
149        self.fields.as_slice()
150    }
151
152    /// Borrow index description entries.
153    #[must_use]
154    pub const fn indexes(&self) -> &[EntityIndexDescription] {
155        self.indexes.as_slice()
156    }
157
158    /// Borrow relation description entries.
159    #[must_use]
160    pub const fn relations(&self) -> &[EntityRelationDescription] {
161        self.relations.as_slice()
162    }
163
164    /// Borrow accepted or generated structural constraint descriptions.
165    #[must_use]
166    pub const fn constraints(&self) -> &[EntityConstraintDescription] {
167        self.constraints.as_slice()
168    }
169
170    /// Return the current accepted physical row-layout identity.
171    #[must_use]
172    pub const fn row_layout_current(&self) -> u32 {
173        self.row_layout_current
174    }
175
176    /// Return the oldest admitted physical row-layout identity.
177    #[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/// Accepted Identity generator policy, exact unsigned domain, and current
189/// lifetime allocation state for one entity.
190#[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    /// Borrow the accepted Identity field name.
225    #[must_use]
226    pub const fn field(&self) -> &str {
227        self.field.as_str()
228    }
229
230    /// Borrow the fixed accepted generator spelling.
231    #[must_use]
232    pub const fn generator(&self) -> &str {
233        self.generator.as_str()
234    }
235
236    /// Borrow the exact accepted unsigned field kind.
237    #[must_use]
238    pub const fn accepted_kind(&self) -> &str {
239        self.accepted_kind.as_str()
240    }
241
242    /// Return the first generated value.
243    #[must_use]
244    pub const fn minimum(&self) -> u128 {
245        self.minimum
246    }
247
248    /// Return the exact accepted lifetime allocation maximum.
249    #[must_use]
250    pub const fn maximum(&self) -> u128 {
251        self.maximum
252    }
253
254    /// Return the greatest committed value, or zero before the first commit.
255    #[must_use]
256    pub const fn high_water(&self) -> u128 {
257        self.high_water
258    }
259
260    /// Return the remaining lifetime allocation capacity.
261    #[must_use]
262    pub const fn remaining(&self) -> u128 {
263        self.remaining
264    }
265
266    /// Return whether the exact accepted unsigned domain is exhausted.
267    #[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/// Current bounded validation-job counters for one activating constraint.
314#[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    /// Borrow the current bounded proof phase.
333    #[must_use]
334    pub const fn phase(&self) -> &str {
335        self.phase.as_str()
336    }
337
338    /// Return the cumulative classified-row count.
339    #[must_use]
340    pub const fn rows_scanned(&self) -> u64 {
341        self.rows_scanned
342    }
343
344    /// Return the cumulative finding count.
345    #[must_use]
346    pub const fn findings_seen(&self) -> u64 {
347        self.findings_seen
348    }
349
350    /// Return the cumulative proof-restart count.
351    #[must_use]
352    pub const fn restarts(&self) -> u64 {
353        self.restarts
354    }
355}
356
357impl EntityConstraintDescription {
358    /// Return the stable entity-local constraint identity.
359    #[must_use]
360    pub const fn id(&self) -> u32 {
361        self.id
362    }
363
364    /// Borrow the stable accepted constraint name.
365    #[must_use]
366    pub const fn name(&self) -> &str {
367        self.name.as_str()
368    }
369
370    /// Borrow the structural constraint kind label.
371    #[must_use]
372    pub const fn kind(&self) -> &str {
373        self.kind.as_str()
374    }
375
376    /// Borrow the constraint origin label.
377    #[must_use]
378    pub const fn origin(&self) -> &str {
379        self.origin.as_str()
380    }
381
382    /// Borrow the validation-state label.
383    #[must_use]
384    pub const fn validation_state(&self) -> &str {
385        self.validation_state.as_str()
386    }
387
388    /// Borrow current bounded validation progress, when activation has begun.
389    #[must_use]
390    pub const fn validation_progress(&self) -> Option<&ConstraintValidationProgressDescription> {
391        self.validation_progress.as_ref()
392    }
393
394    /// Return the referenced field identity for a not-null constraint.
395    #[must_use]
396    pub const fn field_id(&self) -> Option<u32> {
397        self.field_id
398    }
399
400    /// Return the referenced logical index identity for a unique constraint.
401    #[must_use]
402    pub const fn index_id(&self) -> Option<u32> {
403        self.index_id
404    }
405
406    /// Return the referenced logical relation identity.
407    #[must_use]
408    pub const fn relation_id(&self) -> Option<u32> {
409        self.relation_id
410    }
411
412    /// Borrow current accepted field names participating in the constraint.
413    #[must_use]
414    pub const fn fields(&self) -> &[String] {
415        self.fields.as_slice()
416    }
417
418    /// Borrow the current accepted index display name, when applicable.
419    #[must_use]
420    pub fn index(&self) -> Option<&str> {
421        self.index.as_deref()
422    }
423
424    /// Borrow the current accepted relation display name, when applicable.
425    #[must_use]
426    pub fn relation(&self) -> Option<&str> {
427        self.relation.as_deref()
428    }
429
430    /// Borrow the current relation target entity path, when applicable.
431    #[must_use]
432    pub fn target_entity(&self) -> Option<&str> {
433        self.target_entity.as_deref()
434    }
435
436    /// Borrow the derived referential action, when applicable.
437    #[must_use]
438    pub fn action(&self) -> Option<&str> {
439        self.action.as_deref()
440    }
441
442    /// Borrow the derived structural semantics label.
443    #[must_use]
444    pub const fn semantics(&self) -> &str {
445        self.semantics.as_str()
446    }
447
448    /// Borrow the canonical accepted check expression, when applicable.
449    #[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
478///
479/// EntityFieldTemporalFacts
480///
481/// One internally assembled projection of the independent accepted insert and
482/// historical-absence contracts. Nested rows carry an explicitly empty bundle.
483///
484
485struct 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    /// Construct one field description entry.
513    #[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    /// Borrow the field name.
585    #[must_use]
586    pub const fn name(&self) -> &str {
587        self.name.as_str()
588    }
589
590    /// Return the physical row slot for top-level fields.
591    #[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    /// Borrow the rendered field kind label.
601    #[must_use]
602    pub const fn kind(&self) -> &str {
603        self.kind.as_str()
604    }
605
606    /// Return whether this field permits explicit `NULL`.
607    #[must_use]
608    pub const fn nullable(&self) -> bool {
609        self.nullable
610    }
611
612    /// Return whether this field is the primary key.
613    #[must_use]
614    pub const fn primary_key(&self) -> bool {
615        self.primary_key
616    }
617
618    /// Return whether this field is queryable.
619    #[must_use]
620    pub const fn queryable(&self) -> bool {
621        self.queryable
622    }
623
624    /// Borrow the accepted/generated field origin label.
625    #[must_use]
626    pub const fn origin(&self) -> &str {
627        self.origin.as_str()
628    }
629
630    /// Borrow the accepted insert-omission policy label for a top-level field.
631    #[must_use]
632    pub fn insert_omission(&self) -> Option<&str> {
633        self.insert_omission.as_deref()
634    }
635
636    /// Borrow the bounded canonical accepted insert-default rendering.
637    #[must_use]
638    pub fn insert_default(&self) -> Option<&str> {
639        self.insert_default.as_deref()
640    }
641
642    /// Return the accepted insert-default payload byte count.
643    #[must_use]
644    pub const fn insert_default_bytes(&self) -> Option<u32> {
645        self.insert_default_bytes
646    }
647
648    /// Borrow the stable accepted insert-default payload hash.
649    #[must_use]
650    pub fn insert_default_hash(&self) -> Option<&str> {
651        self.insert_default_hash.as_deref()
652    }
653
654    /// Return the row layout that first physically contained this field.
655    #[must_use]
656    pub const fn introduced_in_layout(&self) -> Option<u32> {
657        self.introduced_in_layout
658    }
659
660    /// Borrow the accepted frozen historical-absence rendering.
661    #[must_use]
662    pub fn historical_fill(&self) -> Option<&str> {
663        self.historical_fill.as_deref()
664    }
665
666    /// Return the historical-fill payload byte count when one is stored.
667    #[must_use]
668    pub const fn historical_fill_bytes(&self) -> Option<u32> {
669        self.historical_fill_bytes
670    }
671
672    /// Borrow the stable historical-fill payload hash.
673    #[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    /// Construct one index description entry.
693    #[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    /// Borrow the index name.
704    #[must_use]
705    pub const fn name(&self) -> &str {
706        self.name.as_str()
707    }
708
709    /// Return whether the index enforces uniqueness.
710    #[must_use]
711    pub const fn unique(&self) -> bool {
712        self.unique
713    }
714
715    /// Borrow ordered index field names.
716    #[must_use]
717    pub const fn fields(&self) -> &[String] {
718        self.fields.as_slice()
719    }
720
721    /// Borrow the accepted index origin label.
722    #[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    /// Construct one relation description entry.
743    #[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    /// Borrow the source relation field name.
761    #[must_use]
762    pub const fn field(&self) -> &str {
763        self.field.as_str()
764    }
765
766    /// Borrow the relation target path.
767    #[must_use]
768    pub const fn target_path(&self) -> &str {
769        self.target_path.as_str()
770    }
771
772    /// Borrow the relation target entity name.
773    #[must_use]
774    pub const fn target_entity_name(&self) -> &str {
775        self.target_entity_name.as_str()
776    }
777
778    /// Borrow the relation target store path.
779    #[must_use]
780    pub const fn target_store_path(&self) -> &str {
781        self.target_store_path.as_str()
782    }
783
784    /// Return relation cardinality.
785    #[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
802/// Accepted identity and fingerprint metadata projected into one entity description.
803pub(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    /// Capture the accepted metadata that accompanies persisted schema authority.
812    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
827/// Build one entity-schema description solely from accepted persisted authority.
828pub(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// Assemble the common DESCRIBE payload once field rows have already been built.
882// Callers project relation descriptions from the same authority as their field
883// and index rows, so accepted DESCRIBE output does not fall back to generated
884// relation metadata.
885#[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    // Accepted-schema describe surfaces must follow the stored schema payload,
1275    // not the generated model's current field order.
1276    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
1313///
1314/// DescribeFieldMetadata
1315///
1316/// Field-description metadata selected before one field row is rendered.
1317///
1318
1319struct DescribeFieldMetadata {
1320    kind: String,
1321    nullable: bool,
1322    queryable: bool,
1323    origin: String,
1324}
1325
1326impl DescribeFieldMetadata {
1327    // Build one metadata bundle from already-rendered field facts.
1328    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
1338// Add one already-resolved field row to the stable describe DTO list. The
1339// caller owns where metadata came from: generated model or accepted schema.
1340fn 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    // Nested field rows keep a compact tree marker so table-oriented describe
1350    // output scans as a hierarchy without assigning nested leaves row slots.
1351    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
1366// Render accepted nested leaf descriptors. Nested leaves do not own physical
1367// row slots, so they always appear with the no-slot sentinel in the Candid DTO.
1368fn 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
1517// Write the common text/blob describe label. Both generated and accepted schema
1518// summaries use this path so bounded and explicitly unbounded contracts stay
1519// visibly identical across `DESCRIBE` and `SHOW COLUMNS`.
1520fn 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
1542///
1543/// RenderedTemporalPayload
1544///
1545/// One accepted temporal payload projected as an inseparable bounded value,
1546/// byte count, and stable diagnostic hash.
1547///
1548
1549struct 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
1696// Stream the accepted persisted field-kind label in the stable public
1697// `DESCRIBE` format directly from live schema metadata.
1698fn 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//
1838// TESTS
1839//
1840
1841#[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}