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