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 generated or accepted schema metadata into stable describe surfaces.
5
6use crate::{
7    db::{
8        data::decode_admitted_value_from_accepted_field_contract,
9        relation::{
10            RelationFieldCardinality, RelationFieldMetadata, relation_field_metadata_for_model_iter,
11        },
12        schema::{
13            AcceptedConstraintKind, AcceptedFieldKind, AcceptedFieldPersistenceContract,
14            AcceptedInsertOmissionPolicy, AcceptedRowLayoutRuntimeContract, AcceptedSchemaSnapshot,
15            AcceptedValueCatalogHandle, ConstraintActivationKind, ConstraintActivationSnapshot,
16            ConstraintActivationState, ConstraintOrigin, ConstraintValidationJob, FieldId,
17            PersistedIndexKeyItemSnapshot, PersistedIndexKeySnapshot, PersistedNestedLeafSnapshot,
18            PersistedSchemaSnapshot, RelationId, SchemaHistoricalFill,
19            composite_catalog::{AcceptedCompositeElement, AcceptedCompositeShape},
20            field_type_from_persisted_kind, not_null_constraint_name, output_value_from_runtime,
21            primary_key_constraint_name, render_accepted_check_expr_sql,
22            runtime::AcceptedRowLayoutRuntimeField,
23        },
24    },
25    error::InternalError,
26    model::{
27        entity::EntityModel,
28        field::{
29            CompositeCodec, CompositeElementModel, CompositeShapeModel, FieldDatabaseDefault,
30            FieldKind, FieldModel,
31        },
32    },
33    value::{OutputValue, render_output_value_text},
34};
35use std::fmt::Write;
36
37use candid::CandidType;
38use serde::Deserialize;
39use sha2::{Digest, Sha256};
40
41const ENTITY_FIELD_DESCRIPTION_NO_SLOT: u16 = u16::MAX;
42const MAX_SCHEMA_VALUE_RENDER_CHARS: usize = 128;
43
44#[cfg_attr(
45    doc,
46    doc = "EntitySchemaDescription\n\nStable describe payload for one entity model."
47)]
48#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
49pub struct EntitySchemaDescription {
50    pub(crate) entity_path: String,
51    pub(crate) entity_name: String,
52    pub(crate) primary_key: String,
53    pub(crate) primary_key_fields: Vec<String>,
54    pub(crate) fields: Vec<EntityFieldDescription>,
55    pub(crate) indexes: Vec<EntityIndexDescription>,
56    pub(crate) relations: Vec<EntityRelationDescription>,
57    pub(crate) constraints: Vec<EntityConstraintDescription>,
58    pub(crate) row_layout_current: u32,
59    pub(crate) row_layout_history_floor: u32,
60}
61
62#[cfg_attr(
63    doc,
64    doc = "EntitySchemaCheckDescription\n\nGenerated-vs-accepted schema description payload for one entity."
65)]
66#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
67pub struct EntitySchemaCheckDescription {
68    pub(crate) generated: EntitySchemaDescription,
69    pub(crate) accepted: EntitySchemaDescription,
70}
71
72impl EntitySchemaCheckDescription {
73    /// Construct one generated-vs-accepted schema check payload.
74    #[must_use]
75    pub const fn new(
76        generated: EntitySchemaDescription,
77        accepted: EntitySchemaDescription,
78    ) -> Self {
79        Self {
80            generated,
81            accepted,
82        }
83    }
84
85    /// Borrow the generated schema proposal description.
86    #[must_use]
87    pub const fn generated(&self) -> &EntitySchemaDescription {
88        &self.generated
89    }
90
91    /// Borrow the accepted live-schema description.
92    #[must_use]
93    pub const fn accepted(&self) -> &EntitySchemaDescription {
94        &self.accepted
95    }
96}
97
98impl EntitySchemaDescription {
99    /// Construct one entity schema description payload.
100    #[expect(
101        clippy::too_many_arguments,
102        reason = "schema description construction keeps identity, collections, and layout explicit"
103    )]
104    #[must_use]
105    pub const fn new(
106        entity_path: String,
107        entity_name: String,
108        primary_key: String,
109        primary_key_fields: Vec<String>,
110        fields: Vec<EntityFieldDescription>,
111        indexes: Vec<EntityIndexDescription>,
112        relations: Vec<EntityRelationDescription>,
113        constraints: Vec<EntityConstraintDescription>,
114        row_layout_current: u32,
115        row_layout_history_floor: u32,
116    ) -> Self {
117        Self {
118            entity_path,
119            entity_name,
120            primary_key,
121            primary_key_fields,
122            fields,
123            indexes,
124            relations,
125            constraints,
126            row_layout_current,
127            row_layout_history_floor,
128        }
129    }
130
131    /// Borrow the entity module path.
132    #[must_use]
133    pub const fn entity_path(&self) -> &str {
134        self.entity_path.as_str()
135    }
136
137    /// Borrow the entity display name.
138    #[must_use]
139    pub const fn entity_name(&self) -> &str {
140        self.entity_name.as_str()
141    }
142
143    /// Borrow the rendered primary-key field list.
144    #[must_use]
145    pub const fn primary_key(&self) -> &str {
146        self.primary_key.as_str()
147    }
148
149    /// Borrow ordered primary-key field names.
150    #[must_use]
151    pub const fn primary_key_fields(&self) -> &[String] {
152        self.primary_key_fields.as_slice()
153    }
154
155    /// Borrow field description entries.
156    #[must_use]
157    pub const fn fields(&self) -> &[EntityFieldDescription] {
158        self.fields.as_slice()
159    }
160
161    /// Borrow index description entries.
162    #[must_use]
163    pub const fn indexes(&self) -> &[EntityIndexDescription] {
164        self.indexes.as_slice()
165    }
166
167    /// Borrow relation description entries.
168    #[must_use]
169    pub const fn relations(&self) -> &[EntityRelationDescription] {
170        self.relations.as_slice()
171    }
172
173    /// Borrow accepted or generated structural constraint descriptions.
174    #[must_use]
175    pub const fn constraints(&self) -> &[EntityConstraintDescription] {
176        self.constraints.as_slice()
177    }
178
179    /// Return the current accepted physical row-layout identity.
180    #[must_use]
181    pub const fn row_layout_current(&self) -> u32 {
182        self.row_layout_current
183    }
184
185    /// Return the oldest admitted physical row-layout identity.
186    #[must_use]
187    pub const fn row_layout_history_floor(&self) -> u32 {
188        self.row_layout_history_floor
189    }
190}
191
192#[cfg_attr(
193    doc,
194    doc = "EntityConstraintDescription\n\nOne accepted structural constraint entry in a describe payload."
195)]
196#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
197pub struct EntityConstraintDescription {
198    pub(crate) id: u32,
199    pub(crate) name: String,
200    pub(crate) kind: String,
201    pub(crate) origin: String,
202    pub(crate) validation_state: String,
203    pub(crate) validation_progress: Option<ConstraintValidationProgressDescription>,
204    pub(crate) field_id: Option<u32>,
205    pub(crate) index_id: Option<u32>,
206    pub(crate) relation_id: Option<u32>,
207    pub(crate) fields: Vec<String>,
208    pub(crate) index: Option<String>,
209    pub(crate) relation: Option<String>,
210    pub(crate) target_entity: Option<String>,
211    pub(crate) action: Option<String>,
212    pub(crate) semantics: String,
213    pub(crate) check_sql: Option<String>,
214}
215
216/// Current bounded validation-job counters for one activating constraint.
217#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
218pub struct ConstraintValidationProgressDescription {
219    phase: String,
220    rows_scanned: u64,
221    findings_seen: u64,
222    restarts: u64,
223}
224
225impl ConstraintValidationProgressDescription {
226    fn from_job(job: &ConstraintValidationJob) -> Self {
227        Self {
228            phase: job.phase().as_str().to_string(),
229            rows_scanned: job.rows_scanned(),
230            findings_seen: job.findings_seen(),
231            restarts: job.restarts(),
232        }
233    }
234
235    /// Borrow the current bounded proof phase.
236    #[must_use]
237    pub const fn phase(&self) -> &str {
238        self.phase.as_str()
239    }
240
241    /// Return the cumulative classified-row count.
242    #[must_use]
243    pub const fn rows_scanned(&self) -> u64 {
244        self.rows_scanned
245    }
246
247    /// Return the cumulative finding count.
248    #[must_use]
249    pub const fn findings_seen(&self) -> u64 {
250        self.findings_seen
251    }
252
253    /// Return the cumulative proof-restart count.
254    #[must_use]
255    pub const fn restarts(&self) -> u64 {
256        self.restarts
257    }
258}
259
260impl EntityConstraintDescription {
261    /// Return the stable entity-local constraint identity.
262    #[must_use]
263    pub const fn id(&self) -> u32 {
264        self.id
265    }
266
267    /// Borrow the stable accepted constraint name.
268    #[must_use]
269    pub const fn name(&self) -> &str {
270        self.name.as_str()
271    }
272
273    /// Borrow the structural constraint kind label.
274    #[must_use]
275    pub const fn kind(&self) -> &str {
276        self.kind.as_str()
277    }
278
279    /// Borrow the constraint origin label.
280    #[must_use]
281    pub const fn origin(&self) -> &str {
282        self.origin.as_str()
283    }
284
285    /// Borrow the validation-state label.
286    #[must_use]
287    pub const fn validation_state(&self) -> &str {
288        self.validation_state.as_str()
289    }
290
291    /// Borrow current bounded validation progress, when activation has begun.
292    #[must_use]
293    pub const fn validation_progress(&self) -> Option<&ConstraintValidationProgressDescription> {
294        self.validation_progress.as_ref()
295    }
296
297    /// Return the referenced field identity for a not-null constraint.
298    #[must_use]
299    pub const fn field_id(&self) -> Option<u32> {
300        self.field_id
301    }
302
303    /// Return the referenced logical index identity for a unique constraint.
304    #[must_use]
305    pub const fn index_id(&self) -> Option<u32> {
306        self.index_id
307    }
308
309    /// Return the referenced logical relation identity.
310    #[must_use]
311    pub const fn relation_id(&self) -> Option<u32> {
312        self.relation_id
313    }
314
315    /// Borrow current accepted field names participating in the constraint.
316    #[must_use]
317    pub const fn fields(&self) -> &[String] {
318        self.fields.as_slice()
319    }
320
321    /// Borrow the current accepted index display name, when applicable.
322    #[must_use]
323    pub fn index(&self) -> Option<&str> {
324        self.index.as_deref()
325    }
326
327    /// Borrow the current accepted relation display name, when applicable.
328    #[must_use]
329    pub fn relation(&self) -> Option<&str> {
330        self.relation.as_deref()
331    }
332
333    /// Borrow the current relation target entity path, when applicable.
334    #[must_use]
335    pub fn target_entity(&self) -> Option<&str> {
336        self.target_entity.as_deref()
337    }
338
339    /// Borrow the derived referential action, when applicable.
340    #[must_use]
341    pub fn action(&self) -> Option<&str> {
342        self.action.as_deref()
343    }
344
345    /// Borrow the derived structural semantics label.
346    #[must_use]
347    pub const fn semantics(&self) -> &str {
348        self.semantics.as_str()
349    }
350
351    /// Borrow the canonical accepted check expression, when applicable.
352    #[must_use]
353    pub fn check_sql(&self) -> Option<&str> {
354        self.check_sql.as_deref()
355    }
356}
357
358#[cfg_attr(
359    doc,
360    doc = "EntityFieldDescription\n\nOne field entry in a describe payload."
361)]
362#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
363pub struct EntityFieldDescription {
364    pub(crate) name: String,
365    pub(crate) slot: u16,
366    pub(crate) kind: String,
367    pub(crate) nullable: bool,
368    pub(crate) primary_key: bool,
369    pub(crate) queryable: bool,
370    pub(crate) origin: String,
371    pub(crate) insert_omission: Option<String>,
372    pub(crate) insert_default: Option<String>,
373    pub(crate) insert_default_bytes: Option<u32>,
374    pub(crate) insert_default_hash: Option<String>,
375    pub(crate) introduced_in_layout: Option<u32>,
376    pub(crate) historical_fill: Option<String>,
377    pub(crate) historical_fill_bytes: Option<u32>,
378    pub(crate) historical_fill_hash: Option<String>,
379}
380
381///
382/// EntityFieldTemporalFacts
383///
384/// One internally assembled projection of the independent accepted insert and
385/// historical-absence contracts. Nested rows carry an explicitly empty bundle.
386///
387
388struct EntityFieldTemporalFacts {
389    insert_omission: Option<String>,
390    insert_default: Option<String>,
391    insert_default_bytes: Option<u32>,
392    insert_default_hash: Option<String>,
393    introduced_in_layout: Option<u32>,
394    historical_fill: Option<String>,
395    historical_fill_bytes: Option<u32>,
396    historical_fill_hash: Option<String>,
397}
398
399impl EntityFieldTemporalFacts {
400    const fn nested() -> Self {
401        Self {
402            insert_omission: None,
403            insert_default: None,
404            insert_default_bytes: None,
405            insert_default_hash: None,
406            introduced_in_layout: None,
407            historical_fill: None,
408            historical_fill_bytes: None,
409            historical_fill_hash: None,
410        }
411    }
412
413    fn generated(field: &FieldModel) -> Self {
414        let insert_omission = if field.insert_generation().is_some() {
415            "generated"
416        } else if field.write_management().is_some() {
417            "managed"
418        } else {
419            match field.database_default() {
420                FieldDatabaseDefault::EncodedSlotPayload(_)
421                | FieldDatabaseDefault::AuthoredEnumUnit { .. } => "default",
422                FieldDatabaseDefault::None if field.nullable() => "null",
423                FieldDatabaseDefault::None => "required",
424            }
425        };
426        let (insert_default, insert_default_bytes, insert_default_hash) =
427            generated_insert_default_facts(field.database_default());
428
429        Self {
430            insert_omission: Some(insert_omission.to_string()),
431            insert_default,
432            insert_default_bytes,
433            insert_default_hash,
434            introduced_in_layout: Some(1),
435            historical_fill: Some("reject".to_string()),
436            historical_fill_bytes: None,
437            historical_fill_hash: None,
438        }
439    }
440}
441
442impl EntityFieldDescription {
443    /// Construct one field description entry.
444    #[expect(
445        clippy::too_many_arguments,
446        reason = "schema description construction keeps every temporal field fact explicit"
447    )]
448    #[must_use]
449    pub fn new(
450        name: String,
451        slot: Option<u16>,
452        kind: String,
453        nullable: bool,
454        primary_key: bool,
455        queryable: bool,
456        origin: String,
457        insert_omission: Option<String>,
458        insert_default: Option<String>,
459        insert_default_bytes: Option<u32>,
460        insert_default_hash: Option<String>,
461        introduced_in_layout: Option<u32>,
462        historical_fill: Option<String>,
463        historical_fill_bytes: Option<u32>,
464        historical_fill_hash: Option<String>,
465    ) -> Self {
466        Self::new_with_temporal_facts(
467            name,
468            slot,
469            primary_key,
470            DescribeFieldMetadata::new(kind, nullable, queryable, origin),
471            EntityFieldTemporalFacts {
472                insert_omission,
473                insert_default,
474                insert_default_bytes,
475                insert_default_hash,
476                introduced_in_layout,
477                historical_fill,
478                historical_fill_bytes,
479                historical_fill_hash,
480            },
481        )
482    }
483
484    fn new_with_temporal_facts(
485        name: String,
486        slot: Option<u16>,
487        primary_key: bool,
488        metadata: DescribeFieldMetadata,
489        temporal: EntityFieldTemporalFacts,
490    ) -> Self {
491        let slot = match slot {
492            Some(slot) => slot,
493            None => ENTITY_FIELD_DESCRIPTION_NO_SLOT,
494        };
495
496        Self {
497            name,
498            slot,
499            kind: metadata.kind,
500            nullable: metadata.nullable,
501            primary_key,
502            queryable: metadata.queryable,
503            origin: metadata.origin,
504            insert_omission: temporal.insert_omission,
505            insert_default: temporal.insert_default,
506            insert_default_bytes: temporal.insert_default_bytes,
507            insert_default_hash: temporal.insert_default_hash,
508            introduced_in_layout: temporal.introduced_in_layout,
509            historical_fill: temporal.historical_fill,
510            historical_fill_bytes: temporal.historical_fill_bytes,
511            historical_fill_hash: temporal.historical_fill_hash,
512        }
513    }
514
515    /// Borrow the field name.
516    #[must_use]
517    pub const fn name(&self) -> &str {
518        self.name.as_str()
519    }
520
521    /// Return the physical row slot for top-level fields.
522    #[must_use]
523    pub const fn slot(&self) -> Option<u16> {
524        if self.slot == ENTITY_FIELD_DESCRIPTION_NO_SLOT {
525            None
526        } else {
527            Some(self.slot)
528        }
529    }
530
531    /// Borrow the rendered field kind label.
532    #[must_use]
533    pub const fn kind(&self) -> &str {
534        self.kind.as_str()
535    }
536
537    /// Return whether this field permits explicit `NULL`.
538    #[must_use]
539    pub const fn nullable(&self) -> bool {
540        self.nullable
541    }
542
543    /// Return whether this field is the primary key.
544    #[must_use]
545    pub const fn primary_key(&self) -> bool {
546        self.primary_key
547    }
548
549    /// Return whether this field is queryable.
550    #[must_use]
551    pub const fn queryable(&self) -> bool {
552        self.queryable
553    }
554
555    /// Borrow the accepted/generated field origin label.
556    #[must_use]
557    pub const fn origin(&self) -> &str {
558        self.origin.as_str()
559    }
560
561    /// Borrow the accepted insert-omission policy label for a top-level field.
562    #[must_use]
563    pub fn insert_omission(&self) -> Option<&str> {
564        self.insert_omission.as_deref()
565    }
566
567    /// Borrow the bounded canonical accepted insert-default rendering.
568    #[must_use]
569    pub fn insert_default(&self) -> Option<&str> {
570        self.insert_default.as_deref()
571    }
572
573    /// Return the accepted insert-default payload byte count.
574    #[must_use]
575    pub const fn insert_default_bytes(&self) -> Option<u32> {
576        self.insert_default_bytes
577    }
578
579    /// Borrow the stable accepted insert-default payload hash.
580    #[must_use]
581    pub fn insert_default_hash(&self) -> Option<&str> {
582        self.insert_default_hash.as_deref()
583    }
584
585    /// Return the row layout that first physically contained this field.
586    #[must_use]
587    pub const fn introduced_in_layout(&self) -> Option<u32> {
588        self.introduced_in_layout
589    }
590
591    /// Borrow the accepted frozen historical-absence rendering.
592    #[must_use]
593    pub fn historical_fill(&self) -> Option<&str> {
594        self.historical_fill.as_deref()
595    }
596
597    /// Return the historical-fill payload byte count when one is stored.
598    #[must_use]
599    pub const fn historical_fill_bytes(&self) -> Option<u32> {
600        self.historical_fill_bytes
601    }
602
603    /// Borrow the stable historical-fill payload hash.
604    #[must_use]
605    pub fn historical_fill_hash(&self) -> Option<&str> {
606        self.historical_fill_hash.as_deref()
607    }
608}
609
610#[cfg_attr(
611    doc,
612    doc = "EntityIndexDescription\n\nOne index entry in a describe payload."
613)]
614#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
615pub struct EntityIndexDescription {
616    pub(crate) name: String,
617    pub(crate) unique: bool,
618    pub(crate) fields: Vec<String>,
619    pub(crate) origin: String,
620}
621
622impl EntityIndexDescription {
623    /// Construct one index description entry.
624    #[must_use]
625    pub const fn new(name: String, unique: bool, fields: Vec<String>, origin: String) -> Self {
626        Self {
627            name,
628            unique,
629            fields,
630            origin,
631        }
632    }
633
634    /// Borrow the index name.
635    #[must_use]
636    pub const fn name(&self) -> &str {
637        self.name.as_str()
638    }
639
640    /// Return whether the index enforces uniqueness.
641    #[must_use]
642    pub const fn unique(&self) -> bool {
643        self.unique
644    }
645
646    /// Borrow ordered index field names.
647    #[must_use]
648    pub const fn fields(&self) -> &[String] {
649        self.fields.as_slice()
650    }
651
652    /// Borrow the accepted index origin label.
653    #[must_use]
654    pub const fn origin(&self) -> &str {
655        self.origin.as_str()
656    }
657}
658
659#[cfg_attr(
660    doc,
661    doc = "EntityRelationDescription\n\nOne relation entry in a describe payload."
662)]
663#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
664pub struct EntityRelationDescription {
665    pub(crate) field: String,
666    pub(crate) target_path: String,
667    pub(crate) target_entity_name: String,
668    pub(crate) target_store_path: String,
669    pub(crate) cardinality: EntityRelationCardinality,
670}
671
672impl EntityRelationDescription {
673    /// Construct one relation description entry.
674    #[must_use]
675    pub const fn new(
676        field: String,
677        target_path: String,
678        target_entity_name: String,
679        target_store_path: String,
680        cardinality: EntityRelationCardinality,
681    ) -> Self {
682        Self {
683            field,
684            target_path,
685            target_entity_name,
686            target_store_path,
687            cardinality,
688        }
689    }
690
691    /// Borrow the source relation field name.
692    #[must_use]
693    pub const fn field(&self) -> &str {
694        self.field.as_str()
695    }
696
697    /// Borrow the relation target path.
698    #[must_use]
699    pub const fn target_path(&self) -> &str {
700        self.target_path.as_str()
701    }
702
703    /// Borrow the relation target entity name.
704    #[must_use]
705    pub const fn target_entity_name(&self) -> &str {
706        self.target_entity_name.as_str()
707    }
708
709    /// Borrow the relation target store path.
710    #[must_use]
711    pub const fn target_store_path(&self) -> &str {
712        self.target_store_path.as_str()
713    }
714
715    /// Return relation cardinality.
716    #[must_use]
717    pub const fn cardinality(&self) -> EntityRelationCardinality {
718        self.cardinality
719    }
720}
721
722#[cfg_attr(
723    doc,
724    doc = "EntityRelationCardinality\n\nDescribe relation cardinality."
725)]
726#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
727pub enum EntityRelationCardinality {
728    Single,
729    List,
730    Set,
731}
732
733#[cfg_attr(
734    doc,
735    doc = "Build one stable entity-schema description from one runtime `EntityModel`."
736)]
737#[must_use]
738pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
739    let fields = describe_entity_fields(model);
740    let primary_key_fields = primary_key_field_names_from_model(model);
741    let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
742
743    describe_entity_model_from_description_rows(
744        model.path,
745        model.entity_name,
746        primary_key.as_str(),
747        primary_key_fields,
748        fields,
749        describe_entity_indexes_from_model(model),
750        describe_entity_relations_from_model(model),
751        describe_entity_constraints_from_model(model),
752        1,
753        1,
754    )
755}
756
757#[cfg_attr(
758    doc,
759    doc = "Build one entity-schema description using accepted persisted schema slot metadata."
760)]
761pub(in crate::db) fn describe_entity_model_with_persisted_schema(
762    model: &EntityModel,
763    schema: &AcceptedSchemaSnapshot,
764    value_catalog: &AcceptedValueCatalogHandle,
765    validation_jobs: &[ConstraintValidationJob],
766) -> Result<EntitySchemaDescription, InternalError> {
767    let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
768    let fields = describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)?;
769    let primary_key_fields = schema.primary_key_field_names();
770    let primary_key_fields = if primary_key_fields.is_empty() {
771        vec![model.primary_key.name.to_string()]
772    } else {
773        primary_key_fields
774            .into_iter()
775            .map(str::to_string)
776            .collect::<Vec<_>>()
777    };
778    let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
779
780    Ok(describe_entity_model_from_description_rows(
781        schema.entity_path(),
782        schema.entity_name(),
783        primary_key.as_str(),
784        primary_key_fields,
785        fields,
786        describe_entity_indexes_with_persisted_schema(schema),
787        describe_entity_relations_with_persisted_schema(schema),
788        describe_entity_constraints_with_persisted_schema(schema, value_catalog, validation_jobs)?,
789        row_layout.current_layout_version().get(),
790        row_layout.history_floor().get(),
791    ))
792}
793
794// Assemble the common DESCRIBE payload once field rows have already been built.
795// Callers project relation descriptions from the same authority as their field
796// and index rows, so accepted DESCRIBE output does not fall back to generated
797// relation metadata.
798#[expect(
799    clippy::too_many_arguments,
800    reason = "one final schema DTO assembly keeps every already-owned section explicit"
801)]
802fn describe_entity_model_from_description_rows(
803    entity_path: &str,
804    entity_name: &str,
805    primary_key: &str,
806    primary_key_fields: Vec<String>,
807    fields: Vec<EntityFieldDescription>,
808    indexes: Vec<EntityIndexDescription>,
809    relations: Vec<EntityRelationDescription>,
810    constraints: Vec<EntityConstraintDescription>,
811    row_layout_current: u32,
812    row_layout_history_floor: u32,
813) -> EntitySchemaDescription {
814    EntitySchemaDescription::new(
815        entity_path.to_string(),
816        entity_name.to_string(),
817        primary_key.to_string(),
818        primary_key_fields,
819        fields,
820        indexes,
821        relations,
822        constraints,
823        row_layout_current,
824        row_layout_history_floor,
825    )
826}
827
828fn describe_entity_constraints_from_model(model: &EntityModel) -> Vec<EntityConstraintDescription> {
829    let mut next_id = 1u32;
830    let mut constraints = vec![model_primary_key_constraint(model, next_id)];
831
832    for (slot, field) in model
833        .fields()
834        .iter()
835        .enumerate()
836        .filter(|(_, field)| !field.nullable())
837    {
838        next_id = next_id
839            .checked_add(1)
840            .expect("generated constraint count should fit current model bounds");
841        let field_id = FieldId::from_initial_slot(slot);
842        constraints.push(model_not_null_constraint(next_id, field_id, field.name()));
843    }
844
845    for index in model.indexes().iter().filter(|index| index.is_unique()) {
846        next_id = next_id
847            .checked_add(1)
848            .expect("generated constraint count should fit current model bounds");
849        constraints.push(model_unique_constraint(
850            next_id,
851            u32::from(index.ordinal()),
852            index.name(),
853            index
854                .fields()
855                .iter()
856                .map(|field| (*field).to_string())
857                .collect(),
858        ));
859    }
860
861    for (position, relation) in model.relations().iter().enumerate() {
862        next_id = next_id
863            .checked_add(1)
864            .expect("generated constraint count should fit current model bounds");
865        let relation_id = u32::try_from(position)
866            .ok()
867            .and_then(|position| position.checked_add(1))
868            .and_then(RelationId::new)
869            .expect("generated relation count should fit current model bounds");
870        constraints.push(model_relation_constraint(
871            next_id,
872            relation_id,
873            relation.name(),
874            relation
875                .local_fields()
876                .iter()
877                .map(|field| field.name().to_string())
878                .collect(),
879            relation.target_path(),
880        ));
881    }
882
883    constraints
884}
885
886fn model_primary_key_constraint(model: &EntityModel, id: u32) -> EntityConstraintDescription {
887    let mut description = accepted_constraint_description(
888        id,
889        primary_key_constraint_name(),
890        ConstraintOrigin::Generated,
891    );
892    description.kind = "primary_key".to_string();
893    description.fields = primary_key_field_names_from_model(model);
894    description.semantics = "primary_key_v1".to_string();
895    description
896}
897
898fn model_not_null_constraint(
899    id: u32,
900    field_id: FieldId,
901    field_name: &str,
902) -> EntityConstraintDescription {
903    let mut description = accepted_constraint_description(
904        id,
905        not_null_constraint_name(field_id).as_str(),
906        ConstraintOrigin::Generated,
907    );
908    description.kind = "not_null".to_string();
909    description.field_id = Some(field_id.get());
910    description.fields = vec![field_name.to_string()];
911    description.semantics = "not_null_v1".to_string();
912    description
913}
914
915fn model_unique_constraint(
916    id: u32,
917    index_id: u32,
918    index_name: &str,
919    fields: Vec<String>,
920) -> EntityConstraintDescription {
921    let mut description =
922        accepted_constraint_description(id, index_name, ConstraintOrigin::Generated);
923    description.kind = "unique".to_string();
924    description.index_id = Some(index_id);
925    description.fields = fields;
926    description.index = Some(index_name.to_string());
927    description.semantics = "unique_index_v1".to_string();
928    description
929}
930
931fn model_relation_constraint(
932    id: u32,
933    relation_id: RelationId,
934    relation_name: &str,
935    fields: Vec<String>,
936    target_path: &str,
937) -> EntityConstraintDescription {
938    let mut description =
939        accepted_constraint_description(id, relation_name, ConstraintOrigin::Generated);
940    description.kind = "relation".to_string();
941    description.relation_id = Some(relation_id.get());
942    description.fields = fields;
943    description.relation = Some(relation_name.to_string());
944    description.target_entity = Some(target_path.to_string());
945    description.action = Some("restrict".to_string());
946    description.semantics = "relation_pk_restrict_v1".to_string();
947    description
948}
949
950fn describe_entity_constraints_with_persisted_schema(
951    schema: &AcceptedSchemaSnapshot,
952    value_catalog: &AcceptedValueCatalogHandle,
953    validation_jobs: &[ConstraintValidationJob],
954) -> Result<Vec<EntityConstraintDescription>, InternalError> {
955    let snapshot = schema.persisted_snapshot();
956    let mut descriptions = snapshot
957        .constraints()
958        .iter()
959        .map(|constraint| describe_accepted_constraint(snapshot, value_catalog, constraint))
960        .collect::<Result<Vec<_>, InternalError>>()?;
961    descriptions.extend(
962        snapshot
963            .constraint_activations()
964            .iter()
965            .map(|activation| {
966                let job = validation_jobs
967                    .iter()
968                    .find(|job| job.constraint_id() == activation.id());
969                describe_constraint_activation(snapshot, value_catalog, activation, job)
970            })
971            .collect::<Result<Vec<_>, InternalError>>()?,
972    );
973    if validation_jobs.iter().any(|job| {
974        !snapshot
975            .constraint_activations()
976            .iter()
977            .any(|activation| activation.id() == job.constraint_id())
978    }) {
979        return Err(InternalError::store_invariant());
980    }
981    descriptions.sort_by_key(EntityConstraintDescription::id);
982    Ok(descriptions)
983}
984
985fn describe_accepted_constraint(
986    snapshot: &PersistedSchemaSnapshot,
987    value_catalog: &AcceptedValueCatalogHandle,
988    constraint: &crate::db::schema::AcceptedConstraintSnapshot,
989) -> Result<EntityConstraintDescription, InternalError> {
990    let mut description = accepted_constraint_description(
991        constraint.id().get(),
992        constraint.name(),
993        constraint.origin(),
994    );
995    match constraint.kind() {
996        AcceptedConstraintKind::PrimaryKey => {
997            description.kind = "primary_key".to_string();
998            description.fields = snapshot
999                .primary_key_field_ids()
1000                .iter()
1001                .map(|field_id| accepted_field_name(snapshot, *field_id))
1002                .collect::<Result<Vec<_>, _>>()?;
1003            description.semantics = "primary_key_v1".to_string();
1004        }
1005        AcceptedConstraintKind::NotNull { field_id } => {
1006            description.kind = "not_null".to_string();
1007            description.field_id = Some(field_id.get());
1008            description.fields = vec![accepted_field_name(snapshot, *field_id)?];
1009            description.semantics = "not_null_v1".to_string();
1010        }
1011        AcceptedConstraintKind::Unique { index_id } => {
1012            let index = snapshot
1013                .indexes()
1014                .iter()
1015                .find(|index| index.schema_id() == *index_id)
1016                .ok_or_else(InternalError::store_invariant)?;
1017            description.kind = "unique".to_string();
1018            description.index_id = Some(index_id.get());
1019            description.fields = describe_persisted_index_fields(index.key());
1020            description.index = Some(index.name().to_string());
1021            description.semantics = "unique_index_v1".to_string();
1022        }
1023        AcceptedConstraintKind::Relation { relation_id } => {
1024            let relation = snapshot
1025                .relations()
1026                .iter()
1027                .find(|relation| relation.id() == *relation_id)
1028                .ok_or_else(InternalError::store_invariant)?;
1029            description.kind = "relation".to_string();
1030            description.relation_id = Some(relation_id.get());
1031            description.fields = relation
1032                .local_field_ids()
1033                .iter()
1034                .map(|field_id| accepted_field_name(snapshot, *field_id))
1035                .collect::<Result<Vec<_>, _>>()?;
1036            description.relation = Some(relation.name().to_string());
1037            description.target_entity = Some(relation.target_path().to_string());
1038            description.action = Some("restrict".to_string());
1039            description.semantics = "relation_pk_restrict_v1".to_string();
1040        }
1041        AcceptedConstraintKind::Check { expression } => {
1042            description.kind = "check".to_string();
1043            description.fields = expression
1044                .dependencies()
1045                .into_iter()
1046                .map(|field_id| accepted_field_name(snapshot, field_id))
1047                .collect::<Result<Vec<_>, _>>()?;
1048            description.semantics = "check_expr_v1".to_string();
1049            description.check_sql = Some(render_accepted_check_expr_sql(
1050                expression,
1051                snapshot,
1052                value_catalog,
1053            )?);
1054        }
1055    }
1056    Ok(description)
1057}
1058
1059fn describe_constraint_activation(
1060    snapshot: &PersistedSchemaSnapshot,
1061    value_catalog: &AcceptedValueCatalogHandle,
1062    activation: &ConstraintActivationSnapshot,
1063    validation_job: Option<&ConstraintValidationJob>,
1064) -> Result<EntityConstraintDescription, InternalError> {
1065    let mut description = accepted_constraint_description(
1066        activation.id().get(),
1067        activation.name(),
1068        activation.origin(),
1069    );
1070    match activation.state() {
1071        ConstraintActivationState::EnforcingNewWrites if validation_job.is_none() => {
1072            description.validation_state = "enforcing_new_writes".to_string();
1073        }
1074        ConstraintActivationState::Validating => {
1075            let job = validation_job.ok_or_else(InternalError::store_invariant)?;
1076            job.validate(Some(activation))?;
1077            description.validation_state = "validating".to_string();
1078            description.validation_progress =
1079                Some(ConstraintValidationProgressDescription::from_job(job));
1080        }
1081        ConstraintActivationState::EnforcingNewWrites => {
1082            return Err(InternalError::store_invariant());
1083        }
1084    }
1085    match activation.kind() {
1086        ConstraintActivationKind::NotNull { field_id } => {
1087            description.kind = "not_null".to_string();
1088            description.field_id = Some(field_id.get());
1089            description.fields = vec![accepted_field_name(snapshot, *field_id)?];
1090            description.semantics = "not_null_v1".to_string();
1091        }
1092        ConstraintActivationKind::Unique { index_id } => {
1093            let index = snapshot
1094                .candidate_indexes()
1095                .iter()
1096                .find(|index| index.schema_id() == *index_id)
1097                .ok_or_else(InternalError::store_invariant)?;
1098            description.kind = "unique".to_string();
1099            description.index_id = Some(index_id.get());
1100            description.fields = describe_persisted_index_fields(index.key());
1101            description.index = Some(index.name().to_string());
1102            description.semantics = "unique_index_v1".to_string();
1103        }
1104        ConstraintActivationKind::Relation { relation_id } => {
1105            let relation = snapshot
1106                .candidate_relations()
1107                .iter()
1108                .find(|relation| relation.id() == *relation_id)
1109                .ok_or_else(InternalError::store_invariant)?;
1110            description.kind = "relation".to_string();
1111            description.relation_id = Some(relation_id.get());
1112            description.fields = relation
1113                .local_field_ids()
1114                .iter()
1115                .map(|field_id| accepted_field_name(snapshot, *field_id))
1116                .collect::<Result<Vec<_>, _>>()?;
1117            description.relation = Some(relation.name().to_string());
1118            description.target_entity = Some(relation.target_path().to_string());
1119            description.action = Some("restrict".to_string());
1120            description.semantics = "relation_pk_restrict_v1".to_string();
1121        }
1122        ConstraintActivationKind::Check { expression } => {
1123            description.kind = "check".to_string();
1124            description.fields = expression
1125                .dependencies()
1126                .into_iter()
1127                .map(|field_id| accepted_field_name(snapshot, field_id))
1128                .collect::<Result<Vec<_>, _>>()?;
1129            description.semantics = "check_expr_v1".to_string();
1130            description.check_sql = Some(render_accepted_check_expr_sql(
1131                expression,
1132                snapshot,
1133                value_catalog,
1134            )?);
1135        }
1136    }
1137    Ok(description)
1138}
1139
1140fn accepted_constraint_description(
1141    id: u32,
1142    name: &str,
1143    origin: ConstraintOrigin,
1144) -> EntityConstraintDescription {
1145    EntityConstraintDescription {
1146        id,
1147        name: name.to_string(),
1148        kind: String::new(),
1149        origin: accepted_constraint_origin_label(origin).to_string(),
1150        validation_state: "validated".to_string(),
1151        validation_progress: None,
1152        field_id: None,
1153        index_id: None,
1154        relation_id: None,
1155        fields: Vec::new(),
1156        index: None,
1157        relation: None,
1158        target_entity: None,
1159        action: None,
1160        semantics: String::new(),
1161        check_sql: None,
1162    }
1163}
1164
1165const fn accepted_constraint_origin_label(origin: ConstraintOrigin) -> &'static str {
1166    match origin {
1167        ConstraintOrigin::Generated => "generated",
1168        ConstraintOrigin::SqlDdl => "sql_ddl",
1169    }
1170}
1171
1172fn accepted_field_name(
1173    snapshot: &crate::db::schema::PersistedSchemaSnapshot,
1174    field_id: FieldId,
1175) -> Result<String, InternalError> {
1176    snapshot
1177        .fields()
1178        .iter()
1179        .find(|field| field.id() == field_id)
1180        .map(|field| field.name().to_string())
1181        .ok_or_else(InternalError::store_invariant)
1182}
1183
1184fn describe_entity_relations_from_model(model: &EntityModel) -> Vec<EntityRelationDescription> {
1185    relation_field_metadata_for_model_iter(model)
1186        .map(relation_description_from_metadata)
1187        .collect()
1188}
1189
1190fn primary_key_field_names_from_model(model: &EntityModel) -> Vec<String> {
1191    model
1192        .primary_key_model()
1193        .fields()
1194        .iter()
1195        .map(|field| field.name.to_string())
1196        .collect()
1197}
1198
1199fn render_primary_key_fields(fields: &[String]) -> String {
1200    fields.join(", ")
1201}
1202
1203fn describe_entity_indexes_from_model(model: &EntityModel) -> Vec<EntityIndexDescription> {
1204    let mut indexes = Vec::with_capacity(model.indexes.len());
1205    for index in model.indexes {
1206        indexes.push(EntityIndexDescription::new(
1207            index.name().to_string(),
1208            index.is_unique(),
1209            index
1210                .fields()
1211                .iter()
1212                .map(|field| (*field).to_string())
1213                .collect(),
1214            "generated".to_string(),
1215        ));
1216    }
1217
1218    indexes
1219}
1220
1221fn describe_entity_indexes_with_persisted_schema(
1222    schema: &AcceptedSchemaSnapshot,
1223) -> Vec<EntityIndexDescription> {
1224    schema
1225        .persisted_snapshot()
1226        .indexes()
1227        .iter()
1228        .map(|index| {
1229            EntityIndexDescription::new(
1230                index.name().to_string(),
1231                index.unique(),
1232                describe_persisted_index_fields(index.key()),
1233                if index.generated() {
1234                    "generated".to_string()
1235                } else {
1236                    "ddl".to_string()
1237                },
1238            )
1239        })
1240        .collect()
1241}
1242
1243fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
1244    match key {
1245        PersistedIndexKeySnapshot::FieldPath(paths) => paths
1246            .iter()
1247            .map(|field_path| field_path.path().join("."))
1248            .collect(),
1249        PersistedIndexKeySnapshot::Items(items) => items
1250            .iter()
1251            .map(|item| match item {
1252                PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
1253                PersistedIndexKeyItemSnapshot::Expression(expression) => {
1254                    expression.canonical_text().to_string()
1255                }
1256            })
1257            .collect(),
1258    }
1259}
1260
1261// Build the stable field-description subset once from one runtime model so
1262// metadata surfaces that only need columns do not rebuild indexes and
1263// relations through the heavier DESCRIBE payload path.
1264#[must_use]
1265pub(in crate::db) fn describe_entity_fields(model: &EntityModel) -> Vec<EntityFieldDescription> {
1266    describe_entity_fields_with_slot_lookup(model, |slot, _field| {
1267        Some(u16::try_from(slot).expect("generated field slot should fit in u16"))
1268    })
1269}
1270
1271#[cfg_attr(
1272    doc,
1273    doc = "Build field descriptors using accepted persisted schema slot metadata."
1274)]
1275pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1276    schema: &AcceptedSchemaSnapshot,
1277    value_catalog: &AcceptedValueCatalogHandle,
1278) -> Result<Vec<EntityFieldDescription>, InternalError> {
1279    let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1280    describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1281}
1282
1283fn describe_entity_fields_with_runtime_contract(
1284    schema: &AcceptedSchemaSnapshot,
1285    row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1286    value_catalog: &AcceptedValueCatalogHandle,
1287) -> Result<Vec<EntityFieldDescription>, InternalError> {
1288    let snapshot = schema.persisted_snapshot();
1289    if snapshot.fields().len() != row_layout.fields().len() {
1290        return Err(InternalError::store_invariant());
1291    }
1292    let mut fields = Vec::with_capacity(snapshot.fields().len());
1293
1294    // Accepted-schema describe surfaces must follow the stored schema payload,
1295    // not the generated model's current field order.
1296    for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1297        if field.id() != runtime_field.field_id() {
1298            return Err(InternalError::store_invariant());
1299        }
1300        let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1301        let slot = Some(runtime_field.slot().get());
1302        let metadata = DescribeFieldMetadata::new(
1303            summarize_persisted_field_kind(field.kind(), value_catalog)?,
1304            field.nullable(),
1305            field_type_from_persisted_kind(field.kind())
1306                .value_kind()
1307                .is_queryable(),
1308            field_origin_label(field.generated()),
1309        );
1310        let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1311
1312        push_described_field_row(
1313            &mut fields,
1314            field.name(),
1315            slot,
1316            primary_key,
1317            None,
1318            metadata,
1319            temporal,
1320        );
1321
1322        if !field.nested_leaves().is_empty() {
1323            describe_persisted_nested_leaves(
1324                &mut fields,
1325                field.nested_leaves(),
1326                field_origin_label(field.generated()),
1327                value_catalog,
1328            )?;
1329        }
1330    }
1331
1332    Ok(fields)
1333}
1334
1335// Build model-only field descriptors with an injected top-level slot lookup.
1336// Accepted-schema introspection has a separate catalog-backed entrypoint above.
1337fn describe_entity_fields_with_slot_lookup(
1338    model: &EntityModel,
1339    mut slot_for_field: impl FnMut(usize, &FieldModel) -> Option<u16>,
1340) -> Vec<EntityFieldDescription> {
1341    let mut fields = Vec::with_capacity(model.fields.len());
1342    let primary_key_fields = primary_key_field_names_from_model(model);
1343
1344    for (slot, field) in model.fields.iter().enumerate() {
1345        let primary_key = primary_key_fields
1346            .iter()
1347            .any(|primary_key_field| primary_key_field == field.name);
1348        describe_field_recursive(
1349            &mut fields,
1350            field.name,
1351            slot_for_field(slot, field),
1352            field,
1353            primary_key,
1354            None,
1355            None,
1356        );
1357    }
1358
1359    fields
1360}
1361
1362///
1363/// DescribeFieldMetadata
1364///
1365/// Field-description metadata selected before one field row is rendered.
1366///
1367
1368struct DescribeFieldMetadata {
1369    kind: String,
1370    nullable: bool,
1371    queryable: bool,
1372    origin: String,
1373}
1374
1375impl DescribeFieldMetadata {
1376    // Build one metadata bundle from already-rendered field facts.
1377    const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1378        Self {
1379            kind,
1380            nullable,
1381            queryable,
1382            origin,
1383        }
1384    }
1385}
1386
1387// Add one generated field and any generated composite-record leaves so
1388// DESCRIBE/SHOW COLUMNS expose the same nested rows SQL can project and filter.
1389fn describe_field_recursive(
1390    fields: &mut Vec<EntityFieldDescription>,
1391    name: &str,
1392    slot: Option<u16>,
1393    field: &FieldModel,
1394    primary_key: bool,
1395    tree_prefix: Option<&'static str>,
1396    metadata_override: Option<DescribeFieldMetadata>,
1397) {
1398    let temporal = if slot.is_some() {
1399        EntityFieldTemporalFacts::generated(field)
1400    } else {
1401        EntityFieldTemporalFacts::nested()
1402    };
1403    let metadata = metadata_override.unwrap_or_else(|| {
1404        DescribeFieldMetadata::new(
1405            summarize_field_kind(&field.kind),
1406            field.nullable(),
1407            field.kind.value_kind().is_queryable(),
1408            "generated".to_string(),
1409        )
1410    });
1411
1412    push_described_field_row(
1413        fields,
1414        name,
1415        slot,
1416        primary_key,
1417        tree_prefix,
1418        metadata,
1419        temporal,
1420    );
1421    describe_generated_nested_fields(fields, field.nested_fields());
1422}
1423
1424// Add one already-resolved field row to the stable describe DTO list. The
1425// caller owns where metadata came from: generated model or accepted schema.
1426fn push_described_field_row(
1427    fields: &mut Vec<EntityFieldDescription>,
1428    name: &str,
1429    slot: Option<u16>,
1430    primary_key: bool,
1431    tree_prefix: Option<&'static str>,
1432    metadata: DescribeFieldMetadata,
1433    temporal: EntityFieldTemporalFacts,
1434) {
1435    // Nested field rows keep a compact tree marker so table-oriented describe
1436    // output scans as a hierarchy without assigning nested leaves row slots.
1437    let display_name = if let Some(prefix) = tree_prefix {
1438        format!("{prefix}{name}")
1439    } else {
1440        name.to_string()
1441    };
1442
1443    fields.push(EntityFieldDescription::new_with_temporal_facts(
1444        display_name,
1445        slot,
1446        primary_key,
1447        metadata,
1448        temporal,
1449    ));
1450}
1451
1452// Render generated nested field metadata recursively for model-only
1453// introspection. Accepted introspection consumes persisted catalog-owned leaves.
1454fn describe_generated_nested_fields(
1455    fields: &mut Vec<EntityFieldDescription>,
1456    nested_fields: &[FieldModel],
1457) {
1458    for (index, nested) in nested_fields.iter().enumerate() {
1459        let prefix = if index + 1 == nested_fields.len() {
1460            "└─ "
1461        } else {
1462            "├─ "
1463        };
1464        describe_field_recursive(
1465            fields,
1466            nested.name(),
1467            None,
1468            nested,
1469            false,
1470            Some(prefix),
1471            None,
1472        );
1473    }
1474}
1475
1476// Render accepted nested leaf descriptors. Nested leaves do not own physical
1477// row slots, so they always appear with the no-slot sentinel in the Candid DTO.
1478fn describe_persisted_nested_leaves(
1479    fields: &mut Vec<EntityFieldDescription>,
1480    nested_leaves: &[PersistedNestedLeafSnapshot],
1481    origin: String,
1482    value_catalog: &AcceptedValueCatalogHandle,
1483) -> Result<(), InternalError> {
1484    for (index, leaf) in nested_leaves.iter().enumerate() {
1485        let prefix = if index + 1 == nested_leaves.len() {
1486            "└─ "
1487        } else {
1488            "├─ "
1489        };
1490        let name = leaf.path().last().map_or("", String::as_str);
1491        let metadata = DescribeFieldMetadata::new(
1492            summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1493            leaf.nullable(),
1494            field_type_from_persisted_kind(leaf.kind())
1495                .value_kind()
1496                .is_queryable(),
1497            origin.clone(),
1498        );
1499
1500        push_described_field_row(
1501            fields,
1502            name,
1503            None,
1504            false,
1505            Some(prefix),
1506            metadata,
1507            EntityFieldTemporalFacts::nested(),
1508        );
1509    }
1510
1511    Ok(())
1512}
1513
1514fn field_origin_label(generated: bool) -> String {
1515    if generated {
1516        "generated".to_string()
1517    } else {
1518        "ddl".to_string()
1519    }
1520}
1521
1522fn describe_entity_relations_with_persisted_schema(
1523    schema: &AcceptedSchemaSnapshot,
1524) -> Vec<EntityRelationDescription> {
1525    schema
1526        .persisted_snapshot()
1527        .fields()
1528        .iter()
1529        .filter_map(relation_description_from_persisted_field)
1530        .collect()
1531}
1532
1533fn relation_description_from_persisted_field(
1534    field: &crate::db::schema::PersistedFieldSnapshot,
1535) -> Option<EntityRelationDescription> {
1536    let relation = persisted_relation_description_metadata(field.kind())?;
1537
1538    Some(EntityRelationDescription::new(
1539        field.name().to_string(),
1540        relation.target_path.to_string(),
1541        relation.target_entity_name.to_string(),
1542        relation.target_store_path.to_string(),
1543        relation.cardinality,
1544    ))
1545}
1546
1547struct PersistedRelationDescriptionMetadata<'a> {
1548    target_path: &'a str,
1549    target_entity_name: &'a str,
1550    target_store_path: &'a str,
1551    cardinality: EntityRelationCardinality,
1552}
1553
1554fn persisted_relation_description_metadata(
1555    kind: &AcceptedFieldKind,
1556) -> Option<PersistedRelationDescriptionMetadata<'_>> {
1557    const fn from_relation_kind(
1558        kind: &AcceptedFieldKind,
1559        cardinality: EntityRelationCardinality,
1560    ) -> Option<PersistedRelationDescriptionMetadata<'_>> {
1561        let AcceptedFieldKind::Relation {
1562            target_path,
1563            target_entity_name,
1564            target_store_path,
1565            ..
1566        } = kind
1567        else {
1568            return None;
1569        };
1570
1571        Some(PersistedRelationDescriptionMetadata {
1572            target_path: target_path.as_str(),
1573            target_entity_name: target_entity_name.as_str(),
1574            target_store_path: target_store_path.as_str(),
1575            cardinality,
1576        })
1577    }
1578
1579    match kind {
1580        AcceptedFieldKind::Relation { .. } => {
1581            from_relation_kind(kind, EntityRelationCardinality::Single)
1582        }
1583        AcceptedFieldKind::List(inner) => {
1584            from_relation_kind(inner, EntityRelationCardinality::List)
1585        }
1586        AcceptedFieldKind::Set(inner) => from_relation_kind(inner, EntityRelationCardinality::Set),
1587        _ => None,
1588    }
1589}
1590
1591// Project relation-owned metadata into the stable describe DTO surface.
1592fn relation_description_from_metadata(
1593    metadata: RelationFieldMetadata,
1594) -> EntityRelationDescription {
1595    let cardinality = match metadata.cardinality() {
1596        RelationFieldCardinality::Single => EntityRelationCardinality::Single,
1597        RelationFieldCardinality::List => EntityRelationCardinality::List,
1598        RelationFieldCardinality::Set => EntityRelationCardinality::Set,
1599    };
1600
1601    EntityRelationDescription::new(
1602        metadata.field_name().to_string(),
1603        metadata.target_path().to_string(),
1604        metadata.target_entity_name().to_string(),
1605        metadata.target_store_path().to_string(),
1606        cardinality,
1607    )
1608}
1609
1610#[cfg_attr(doc, doc = "Render one stable field-kind label for describe output.")]
1611fn summarize_field_kind(kind: &FieldKind) -> String {
1612    let mut out = String::new();
1613    write_field_kind_summary(&mut out, kind);
1614
1615    out
1616}
1617
1618// Stream one stable field-kind label directly into the output buffer so
1619// describe/sql surfaces do not retain a large recursive `format!` family.
1620fn write_field_kind_summary(out: &mut String, kind: &FieldKind) {
1621    if let Some(name) = kind.describe_kind_name() {
1622        out.push_str(name);
1623        return;
1624    }
1625
1626    match kind {
1627        FieldKind::Blob { max_len } => {
1628            write_length_bounded_field_kind_summary(out, "blob", *max_len);
1629        }
1630        FieldKind::Decimal { scale } => {
1631            let _ = write!(out, "decimal(scale={scale})");
1632        }
1633        FieldKind::IntBig { max_bytes } => {
1634            write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
1635        }
1636        FieldKind::Enum { path, .. } => {
1637            out.push_str("enum(");
1638            out.push_str(path);
1639            out.push(')');
1640        }
1641        FieldKind::Text { max_len } => {
1642            write_length_bounded_field_kind_summary(out, "text", *max_len);
1643        }
1644        FieldKind::Relation {
1645            target_entity_name,
1646            key_kind,
1647            ..
1648        } => {
1649            out.push_str("relation(target=");
1650            out.push_str(target_entity_name);
1651            out.push_str(", key=");
1652            write_field_kind_summary(out, key_kind);
1653            out.push(')');
1654        }
1655        FieldKind::List(inner) => {
1656            out.push_str("list<");
1657            write_field_kind_summary(out, inner);
1658            out.push('>');
1659        }
1660        FieldKind::Set(inner) => {
1661            out.push_str("set<");
1662            write_field_kind_summary(out, inner);
1663            out.push('>');
1664        }
1665        FieldKind::Map { key, value } => {
1666            out.push_str("map<");
1667            write_field_kind_summary(out, key);
1668            out.push_str(", ");
1669            write_field_kind_summary(out, value);
1670            out.push('>');
1671        }
1672        FieldKind::Composite { path, codec, shape } => {
1673            out.push_str("composite(path=");
1674            out.push_str(path);
1675            out.push_str(", codec=");
1676            write_composite_codec_summary(out, *codec);
1677            out.push_str(", shape=");
1678            write_generated_composite_shape_summary(out, shape);
1679            out.push(')');
1680        }
1681        FieldKind::Account
1682        | FieldKind::Bool
1683        | FieldKind::Date
1684        | FieldKind::Duration
1685        | FieldKind::Float32
1686        | FieldKind::Float64
1687        | FieldKind::Int8
1688        | FieldKind::Int16
1689        | FieldKind::Int32
1690        | FieldKind::Int64
1691        | FieldKind::Int128
1692        | FieldKind::Principal
1693        | FieldKind::Subaccount
1694        | FieldKind::Timestamp
1695        | FieldKind::Nat8
1696        | FieldKind::Nat16
1697        | FieldKind::Nat32
1698        | FieldKind::Nat64
1699        | FieldKind::Nat128
1700        | FieldKind::Ulid
1701        | FieldKind::Unit => unreachable!("schema describe invariant"),
1702        FieldKind::NatBig { max_bytes } => {
1703            write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
1704        }
1705    }
1706}
1707
1708fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1709    match codec {
1710        CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1711    }
1712}
1713
1714fn write_generated_composite_shape_summary(out: &mut String, shape: &CompositeShapeModel) {
1715    match shape {
1716        CompositeShapeModel::Record(fields) => {
1717            out.push_str("record{");
1718            for (index, field) in fields.iter().enumerate() {
1719                if index > 0 {
1720                    out.push_str(", ");
1721                }
1722                out.push_str(field.name());
1723                out.push(':');
1724                write_field_kind_summary(out, &field.kind());
1725                write_composite_nullability_summary(out, field.nullable());
1726            }
1727            out.push('}');
1728        }
1729        CompositeShapeModel::Tuple(elements) => {
1730            out.push_str("tuple<");
1731            for (index, element) in elements.iter().enumerate() {
1732                if index > 0 {
1733                    out.push_str(", ");
1734                }
1735                write_generated_composite_element_summary(out, element);
1736            }
1737            out.push('>');
1738        }
1739        CompositeShapeModel::Newtype(inner) => {
1740            out.push_str("newtype<");
1741            write_generated_composite_element_summary(out, inner);
1742            out.push('>');
1743        }
1744    }
1745}
1746
1747fn write_generated_composite_element_summary(out: &mut String, element: &CompositeElementModel) {
1748    write_field_kind_summary(out, &element.kind());
1749    write_composite_nullability_summary(out, element.nullable());
1750}
1751
1752fn write_accepted_composite_shape_summary(
1753    out: &mut String,
1754    shape: &AcceptedCompositeShape,
1755    value_catalog: &AcceptedValueCatalogHandle,
1756) -> Result<(), InternalError> {
1757    match shape {
1758        AcceptedCompositeShape::Record(fields) => {
1759            out.push_str("record{");
1760            for (index, field) in fields.iter().enumerate() {
1761                if index > 0 {
1762                    out.push_str(", ");
1763                }
1764                out.push_str(field.name());
1765                out.push(':');
1766                write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1767            }
1768            out.push('}');
1769        }
1770        AcceptedCompositeShape::Tuple(elements) => {
1771            out.push_str("tuple<");
1772            for (index, element) in elements.iter().enumerate() {
1773                if index > 0 {
1774                    out.push_str(", ");
1775                }
1776                write_accepted_composite_element_summary(out, element, value_catalog)?;
1777            }
1778            out.push('>');
1779        }
1780        AcceptedCompositeShape::Newtype(inner) => {
1781            out.push_str("newtype<");
1782            write_accepted_composite_element_summary(out, inner, value_catalog)?;
1783            out.push('>');
1784        }
1785    }
1786
1787    Ok(())
1788}
1789
1790fn write_accepted_composite_element_summary(
1791    out: &mut String,
1792    element: &AcceptedCompositeElement,
1793    value_catalog: &AcceptedValueCatalogHandle,
1794) -> Result<(), InternalError> {
1795    write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1796    write_composite_nullability_summary(out, element.nullable());
1797    Ok(())
1798}
1799
1800fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1801    if nullable {
1802        out.push('?');
1803    }
1804}
1805
1806trait DescribeKindName {
1807    fn describe_kind_name(&self) -> Option<&'static str>;
1808}
1809
1810impl DescribeKindName for FieldKind {
1811    fn describe_kind_name(&self) -> Option<&'static str> {
1812        Some(match self {
1813            Self::Account => "account",
1814            Self::Bool => "bool",
1815            Self::Date => "date",
1816            Self::Duration => "duration",
1817            Self::Float32 => "float32",
1818            Self::Float64 => "float64",
1819            Self::Int8 => "int8",
1820            Self::Int16 => "int16",
1821            Self::Int32 => "int32",
1822            Self::Int64 => "int64",
1823            Self::Int128 => "int128",
1824            Self::Principal => "principal",
1825            Self::Subaccount => "subaccount",
1826            Self::Timestamp => "timestamp",
1827            Self::Nat8 => "nat8",
1828            Self::Nat16 => "nat16",
1829            Self::Nat32 => "nat32",
1830            Self::Nat64 => "nat64",
1831            Self::Nat128 => "nat128",
1832            Self::Ulid => "ulid",
1833            Self::Unit => "unit",
1834            Self::Blob { .. }
1835            | Self::Decimal { .. }
1836            | Self::Enum { .. }
1837            | Self::IntBig { .. }
1838            | Self::NatBig { .. }
1839            | Self::Text { .. }
1840            | Self::Relation { .. }
1841            | Self::List(_)
1842            | Self::Set(_)
1843            | Self::Map { .. }
1844            | Self::Composite { .. } => return None,
1845        })
1846    }
1847}
1848
1849// Write the common text/blob describe label. Both generated and accepted schema
1850// summaries use this path so bounded and explicitly unbounded contracts stay
1851// visibly identical across `DESCRIBE` and `SHOW COLUMNS`.
1852fn write_length_bounded_field_kind_summary(
1853    out: &mut String,
1854    kind_name: &str,
1855    max_len: Option<u32>,
1856) {
1857    out.push_str(kind_name);
1858    if let Some(max_len) = max_len {
1859        out.push_str("(max_len=");
1860        out.push_str(&max_len.to_string());
1861        out.push(')');
1862    } else {
1863        out.push_str("(unbounded)");
1864    }
1865}
1866
1867fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1868    out.push_str(kind_name);
1869    out.push_str("(max_bytes=");
1870    out.push_str(&max_bytes.to_string());
1871    out.push(')');
1872}
1873
1874// Project generated proposal metadata without pretending that generated code
1875// owns accepted runtime semantics. Encoded generated defaults can expose their
1876// exact byte identity; accepted introspection below additionally decodes them.
1877fn generated_insert_default_facts(
1878    default: FieldDatabaseDefault,
1879) -> (Option<String>, Option<u32>, Option<String>) {
1880    match default {
1881        FieldDatabaseDefault::None => (None, None, None),
1882        FieldDatabaseDefault::EncodedSlotPayload(payload) => {
1883            let bytes = u32::try_from(payload.len()).ok();
1884            let hash = short_default_payload_fingerprint(payload);
1885            (
1886                Some(format!(
1887                    "slot_payload(bytes={}, sha256={hash})",
1888                    payload.len()
1889                )),
1890                bytes,
1891                Some(hash),
1892            )
1893        }
1894        FieldDatabaseDefault::AuthoredEnumUnit { enum_path, variant } => {
1895            (Some(format!("{enum_path}::{variant}")), None, None)
1896        }
1897    }
1898}
1899
1900///
1901/// RenderedTemporalPayload
1902///
1903/// One accepted temporal payload projected as an inseparable bounded value,
1904/// byte count, and stable diagnostic hash.
1905///
1906
1907struct RenderedTemporalPayload {
1908    value: String,
1909    bytes: u32,
1910    hash: String,
1911}
1912
1913fn accepted_field_temporal_facts(
1914    field: &AcceptedRowLayoutRuntimeField<'_>,
1915    value_catalog: &AcceptedValueCatalogHandle,
1916) -> Result<EntityFieldTemporalFacts, InternalError> {
1917    let write_policy = field.write_policy();
1918    let insert_omission = if write_policy.insert_generation().is_some() {
1919        "generated"
1920    } else if write_policy.write_management().is_some() {
1921        "managed"
1922    } else {
1923        match field.insert_omission_policy() {
1924            AcceptedInsertOmissionPolicy::NullIfMissing => "null",
1925            AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
1926            AcceptedInsertOmissionPolicy::Required => "required",
1927        }
1928    };
1929    let insert_default = field
1930        .insert_default()
1931        .slot_payload()
1932        .map(|payload| accepted_payload_facts(field, value_catalog, payload))
1933        .transpose()?;
1934    let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
1935        Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
1936        None => (None, None, None),
1937    };
1938    let (historical_fill, historical_fill_bytes, historical_fill_hash) =
1939        match field.historical_fill() {
1940            SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
1941            SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
1942            SchemaHistoricalFill::SlotPayload(payload) => {
1943                let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
1944                (
1945                    Some(rendered.value),
1946                    Some(rendered.bytes),
1947                    Some(rendered.hash),
1948                )
1949            }
1950        };
1951
1952    Ok(EntityFieldTemporalFacts {
1953        insert_omission: Some(insert_omission.to_string()),
1954        insert_default,
1955        insert_default_bytes,
1956        insert_default_hash,
1957        introduced_in_layout: Some(field.introduced_in_layout().get()),
1958        historical_fill,
1959        historical_fill_bytes,
1960        historical_fill_hash,
1961    })
1962}
1963
1964fn accepted_payload_facts(
1965    field: &AcceptedRowLayoutRuntimeField<'_>,
1966    value_catalog: &AcceptedValueCatalogHandle,
1967    payload: &[u8],
1968) -> Result<RenderedTemporalPayload, InternalError> {
1969    let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
1970        .map_err(|_| InternalError::store_invariant())?;
1971    let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
1972    let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.value())
1973        .map_err(|_| InternalError::store_invariant())?;
1974    let hash = short_default_payload_fingerprint(payload);
1975    let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
1976    let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
1977
1978    Ok(RenderedTemporalPayload {
1979        value: rendered,
1980        bytes,
1981        hash,
1982    })
1983}
1984
1985fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
1986    let rendered = match value {
1987        OutputValue::Text(value) => format!("'{}'", value.escape_default()),
1988        _ => render_output_value_text(value),
1989    };
1990    if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
1991        return rendered;
1992    }
1993
1994    format!(
1995        "{}(bytes={}, sha256={})",
1996        output_value_kind_label(value),
1997        payload.len(),
1998        hash,
1999    )
2000}
2001
2002const fn output_value_kind_label(value: &OutputValue) -> &'static str {
2003    match value {
2004        OutputValue::Account(_) => "account",
2005        OutputValue::Blob(_) => "blob",
2006        OutputValue::Bool(_) => "bool",
2007        OutputValue::Date(_) => "date",
2008        OutputValue::Decimal(_) => "decimal",
2009        OutputValue::Duration(_) => "duration",
2010        OutputValue::Enum(_) => "enum",
2011        OutputValue::Float32(_) => "float32",
2012        OutputValue::Float64(_) => "float64",
2013        OutputValue::Int64(_) => "int64",
2014        OutputValue::Int128(_) => "int128",
2015        OutputValue::IntBig(_) => "int_big",
2016        OutputValue::List(_) => "list",
2017        OutputValue::Map(_) => "map",
2018        OutputValue::Null => "null",
2019        OutputValue::Principal(_) => "principal",
2020        OutputValue::Subaccount(_) => "subaccount",
2021        OutputValue::Text(_) => "text",
2022        OutputValue::Timestamp(_) => "timestamp",
2023        OutputValue::Nat64(_) => "nat64",
2024        OutputValue::Nat128(_) => "nat128",
2025        OutputValue::NatBig(_) => "nat_big",
2026        OutputValue::Ulid(_) => "ulid",
2027        OutputValue::Unit => "unit",
2028    }
2029}
2030
2031fn short_default_payload_fingerprint(payload: &[u8]) -> String {
2032    let digest = Sha256::digest(payload);
2033    let mut out = String::with_capacity(16);
2034    for byte in &digest[..8] {
2035        let _ = write!(out, "{byte:02x}");
2036    }
2037    out
2038}
2039
2040#[cfg_attr(
2041    doc,
2042    doc = "Render one stable field-kind label from accepted persisted schema metadata."
2043)]
2044fn summarize_persisted_field_kind(
2045    kind: &AcceptedFieldKind,
2046    value_catalog: &AcceptedValueCatalogHandle,
2047) -> Result<String, InternalError> {
2048    let mut out = String::new();
2049    write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
2050
2051    Ok(out)
2052}
2053
2054// Stream the accepted persisted field-kind label in the same public format as
2055// generated `FieldKind` summaries. Top-level live-schema metadata can then
2056// drive DESCRIBE output without converting back into generated static types.
2057fn write_persisted_field_kind_summary(
2058    out: &mut String,
2059    kind: &AcceptedFieldKind,
2060    value_catalog: &AcceptedValueCatalogHandle,
2061) -> Result<(), InternalError> {
2062    if let Some(name) = kind.describe_kind_name() {
2063        out.push_str(name);
2064        return Ok(());
2065    }
2066
2067    match kind {
2068        AcceptedFieldKind::Blob { max_len } => {
2069            write_length_bounded_field_kind_summary(out, "blob", *max_len);
2070        }
2071        AcceptedFieldKind::Decimal { scale } => {
2072            let _ = write!(out, "decimal(scale={scale})");
2073        }
2074        AcceptedFieldKind::IntBig { max_bytes } => {
2075            write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
2076        }
2077        AcceptedFieldKind::Enum { type_id } => {
2078            let definition = value_catalog
2079                .enum_catalog()
2080                .enum_type(*type_id)
2081                .ok_or_else(InternalError::store_invariant)?;
2082            out.push_str("enum(");
2083            out.push_str(definition.path());
2084            out.push(')');
2085        }
2086        AcceptedFieldKind::Text { max_len } => {
2087            write_length_bounded_field_kind_summary(out, "text", *max_len);
2088        }
2089        AcceptedFieldKind::Relation {
2090            target_entity_name,
2091            key_kind,
2092            ..
2093        } => {
2094            out.push_str("relation(target=");
2095            out.push_str(target_entity_name);
2096            out.push_str(", key=");
2097            write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
2098            out.push(')');
2099        }
2100        AcceptedFieldKind::List(inner) => {
2101            out.push_str("list<");
2102            write_persisted_field_kind_summary(out, inner, value_catalog)?;
2103            out.push('>');
2104        }
2105        AcceptedFieldKind::Set(inner) => {
2106            out.push_str("set<");
2107            write_persisted_field_kind_summary(out, inner, value_catalog)?;
2108            out.push('>');
2109        }
2110        AcceptedFieldKind::Map { key, value } => {
2111            out.push_str("map<");
2112            write_persisted_field_kind_summary(out, key, value_catalog)?;
2113            out.push_str(", ");
2114            write_persisted_field_kind_summary(out, value, value_catalog)?;
2115            out.push('>');
2116        }
2117        AcceptedFieldKind::Composite { type_id } => {
2118            let composite_catalog = value_catalog.composite_catalog();
2119            let definition = composite_catalog
2120                .composite_type(*type_id)
2121                .ok_or_else(InternalError::store_invariant)?;
2122            out.push_str("composite(path=");
2123            out.push_str(definition.path());
2124            out.push_str(", codec=");
2125            write_composite_codec_summary(out, definition.codec());
2126            out.push_str(", shape=");
2127            write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
2128            out.push(')');
2129        }
2130        AcceptedFieldKind::Account
2131        | AcceptedFieldKind::Bool
2132        | AcceptedFieldKind::Date
2133        | AcceptedFieldKind::Duration
2134        | AcceptedFieldKind::Float32
2135        | AcceptedFieldKind::Float64
2136        | AcceptedFieldKind::Int8
2137        | AcceptedFieldKind::Int16
2138        | AcceptedFieldKind::Int32
2139        | AcceptedFieldKind::Int64
2140        | AcceptedFieldKind::Int128
2141        | AcceptedFieldKind::Principal
2142        | AcceptedFieldKind::Subaccount
2143        | AcceptedFieldKind::Timestamp
2144        | AcceptedFieldKind::Nat8
2145        | AcceptedFieldKind::Nat16
2146        | AcceptedFieldKind::Nat32
2147        | AcceptedFieldKind::Nat64
2148        | AcceptedFieldKind::Nat128
2149        | AcceptedFieldKind::Ulid
2150        | AcceptedFieldKind::Unit => unreachable!("schema describe invariant"),
2151        AcceptedFieldKind::NatBig { max_bytes } => {
2152            write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
2153        }
2154    }
2155
2156    Ok(())
2157}
2158
2159impl DescribeKindName for AcceptedFieldKind {
2160    fn describe_kind_name(&self) -> Option<&'static str> {
2161        Some(match self {
2162            Self::Account => "account",
2163            Self::Bool => "bool",
2164            Self::Date => "date",
2165            Self::Duration => "duration",
2166            Self::Float32 => "float32",
2167            Self::Float64 => "float64",
2168            Self::Int8 => "int8",
2169            Self::Int16 => "int16",
2170            Self::Int32 => "int32",
2171            Self::Int64 => "int64",
2172            Self::Int128 => "int128",
2173            Self::Principal => "principal",
2174            Self::Subaccount => "subaccount",
2175            Self::Timestamp => "timestamp",
2176            Self::Nat8 => "nat8",
2177            Self::Nat16 => "nat16",
2178            Self::Nat32 => "nat32",
2179            Self::Nat64 => "nat64",
2180            Self::Nat128 => "nat128",
2181            Self::Ulid => "ulid",
2182            Self::Unit => "unit",
2183            Self::Blob { .. }
2184            | Self::Decimal { .. }
2185            | Self::Enum { .. }
2186            | Self::IntBig { .. }
2187            | Self::NatBig { .. }
2188            | Self::Text { .. }
2189            | Self::Relation { .. }
2190            | Self::List(_)
2191            | Self::Set(_)
2192            | Self::Map { .. }
2193            | Self::Composite { .. } => return None,
2194        })
2195    }
2196}
2197
2198//
2199// TESTS
2200//
2201
2202#[cfg(test)]
2203mod tests;