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