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