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