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            query_field_kind_from_persisted_kind, 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    icydb_schema::compact_sort_unstable_by(&mut descriptions, |left, right| {
1160        (left.id(), left.validation_state() != "validated")
1161            .cmp(&(right.id(), right.validation_state() != "validated"))
1162    });
1163    Ok(descriptions)
1164}
1165
1166fn describe_accepted_constraint(
1167    snapshot: &PersistedSchemaSnapshot,
1168    value_catalog: &AcceptedValueCatalogHandle,
1169    constraint: &crate::db::schema::AcceptedConstraintSnapshot,
1170) -> Result<EntityConstraintDescription, InternalError> {
1171    let mut description = accepted_constraint_description(
1172        constraint.id().get(),
1173        constraint.name(),
1174        constraint.origin(),
1175    );
1176    match constraint.kind() {
1177        AcceptedConstraintKind::PrimaryKey => {
1178            description.kind = "primary_key".to_string();
1179            description.fields = snapshot
1180                .primary_key_field_ids()
1181                .iter()
1182                .map(|field_id| accepted_field_name(snapshot, *field_id))
1183                .collect::<Result<Vec<_>, _>>()?;
1184            description.semantics = "primary_key_v1".to_string();
1185        }
1186        AcceptedConstraintKind::NotNull { field_id } => {
1187            description.kind = "not_null".to_string();
1188            description.field_id = Some(field_id.get());
1189            description.fields = vec![accepted_field_name(snapshot, *field_id)?];
1190            description.semantics = "not_null_v1".to_string();
1191        }
1192        AcceptedConstraintKind::Unique { index_id } => {
1193            let index = snapshot
1194                .indexes()
1195                .iter()
1196                .find(|index| index.schema_id() == *index_id)
1197                .ok_or_else(InternalError::store_invariant)?;
1198            apply_unique_index_description(&mut description, index);
1199        }
1200        AcceptedConstraintKind::Relation { relation_id } => {
1201            let relation = snapshot
1202                .relations()
1203                .iter()
1204                .find(|relation| relation.id() == *relation_id)
1205                .ok_or_else(InternalError::store_invariant)?;
1206            description.kind = "relation".to_string();
1207            description.relation_id = Some(relation_id.get());
1208            description.fields = relation
1209                .local_field_ids()
1210                .iter()
1211                .map(|field_id| accepted_field_name(snapshot, *field_id))
1212                .collect::<Result<Vec<_>, _>>()?;
1213            description.relation = Some(relation.name().to_string());
1214            description.target_entity = Some(relation.target_path().to_string());
1215            description.action = Some("restrict".to_string());
1216            description.semantics = "relation_pk_restrict_v1".to_string();
1217        }
1218        AcceptedConstraintKind::Check { expression } => {
1219            description.kind = "check".to_string();
1220            description.fields = expression
1221                .dependencies()
1222                .into_iter()
1223                .map(|field_id| accepted_field_name(snapshot, field_id))
1224                .collect::<Result<Vec<_>, _>>()?;
1225            description.semantics = "check_expr_v1".to_string();
1226            description.check_sql = Some(render_accepted_check_expr_sql(
1227                expression,
1228                snapshot,
1229                value_catalog,
1230            )?);
1231        }
1232        AcceptedConstraintKind::TargetedRule { target, operation } => {
1233            description.kind = "targeted_rule".to_string();
1234            description.field_id = Some(target.root_field_id().get());
1235            description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1236            description.semantics = match operation.as_ref() {
1237                crate::db::schema::AcceptedRuleOperation::LengthRangeInclusive { .. } => {
1238                    "targeted_length_range_v1"
1239                }
1240                crate::db::schema::AcceptedRuleOperation::MultipleOf { .. } => {
1241                    "targeted_multiple_of_v1"
1242                }
1243                crate::db::schema::AcceptedRuleOperation::NumericMaximumInclusive { .. } => {
1244                    "targeted_numeric_maximum_v1"
1245                }
1246                crate::db::schema::AcceptedRuleOperation::NumericMinimumInclusive { .. } => {
1247                    "targeted_numeric_minimum_v1"
1248                }
1249                crate::db::schema::AcceptedRuleOperation::NumericRangeInclusive { .. } => {
1250                    "targeted_numeric_range_v1"
1251                }
1252            }
1253            .to_string();
1254        }
1255    }
1256    Ok(description)
1257}
1258
1259fn describe_constraint_activation(
1260    snapshot: &PersistedSchemaSnapshot,
1261    value_catalog: &AcceptedValueCatalogHandle,
1262    activation: &ConstraintActivationSnapshot,
1263    validation_job: Option<&ConstraintValidationJob>,
1264) -> Result<EntityConstraintDescription, InternalError> {
1265    let mut description = accepted_constraint_description(
1266        activation.id().get(),
1267        activation.name(),
1268        activation.origin(),
1269    );
1270    match activation.state() {
1271        ConstraintActivationState::EnforcingNewWrites if validation_job.is_none() => {
1272            description.validation_state = "enforcing_new_writes".to_string();
1273        }
1274        ConstraintActivationState::Validating => {
1275            let job = validation_job.ok_or_else(InternalError::store_invariant)?;
1276            job.validate(Some(activation))?;
1277            description.validation_state = "validating".to_string();
1278            description.validation_progress =
1279                Some(ConstraintValidationProgressDescription::from_job(job));
1280        }
1281        ConstraintActivationState::EnforcingNewWrites => {
1282            return Err(InternalError::store_invariant());
1283        }
1284    }
1285    match activation.kind() {
1286        ConstraintActivationKind::NotNull { field_id } => {
1287            description.kind = "not_null".to_string();
1288            description.field_id = Some(field_id.get());
1289            description.fields = vec![accepted_field_name(snapshot, *field_id)?];
1290            description.semantics = "not_null_v1".to_string();
1291        }
1292        ConstraintActivationKind::Unique { index_id } => {
1293            let index = snapshot
1294                .candidate_indexes()
1295                .iter()
1296                .find(|index| index.schema_id() == *index_id)
1297                .ok_or_else(InternalError::store_invariant)?;
1298            apply_unique_index_description(&mut description, index);
1299        }
1300        ConstraintActivationKind::Relation { relation_id } => {
1301            let relation = snapshot
1302                .candidate_relations()
1303                .iter()
1304                .find(|relation| relation.id() == *relation_id)
1305                .ok_or_else(InternalError::store_invariant)?;
1306            description.kind = "relation".to_string();
1307            description.relation_id = Some(relation_id.get());
1308            description.fields = relation
1309                .local_field_ids()
1310                .iter()
1311                .map(|field_id| accepted_field_name(snapshot, *field_id))
1312                .collect::<Result<Vec<_>, _>>()?;
1313            description.relation = Some(relation.name().to_string());
1314            description.target_entity = Some(relation.target_path().to_string());
1315            description.action = Some("restrict".to_string());
1316            description.semantics = "relation_pk_restrict_v1".to_string();
1317        }
1318        ConstraintActivationKind::Check { expression } => {
1319            description.kind = "check".to_string();
1320            description.fields = expression
1321                .dependencies()
1322                .into_iter()
1323                .map(|field_id| accepted_field_name(snapshot, field_id))
1324                .collect::<Result<Vec<_>, _>>()?;
1325            description.semantics = "check_expr_v1".to_string();
1326            description.check_sql = Some(render_accepted_check_expr_sql(
1327                expression,
1328                snapshot,
1329                value_catalog,
1330            )?);
1331        }
1332        ConstraintActivationKind::TargetedRule { target, operation } => {
1333            description.kind = "targeted_rule".to_string();
1334            description.field_id = Some(target.root_field_id().get());
1335            description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1336            description.semantics = match operation.as_ref() {
1337                crate::db::schema::AcceptedRuleOperation::LengthRangeInclusive { .. } => {
1338                    "targeted_length_range_v1"
1339                }
1340                crate::db::schema::AcceptedRuleOperation::MultipleOf { .. } => {
1341                    "targeted_multiple_of_v1"
1342                }
1343                crate::db::schema::AcceptedRuleOperation::NumericMaximumInclusive { .. } => {
1344                    "targeted_numeric_maximum_v1"
1345                }
1346                crate::db::schema::AcceptedRuleOperation::NumericMinimumInclusive { .. } => {
1347                    "targeted_numeric_minimum_v1"
1348                }
1349                crate::db::schema::AcceptedRuleOperation::NumericRangeInclusive { .. } => {
1350                    "targeted_numeric_range_v1"
1351                }
1352            }
1353            .to_string();
1354        }
1355    }
1356    Ok(description)
1357}
1358
1359fn accepted_constraint_description(
1360    id: u32,
1361    name: &str,
1362    origin: ConstraintOrigin,
1363) -> EntityConstraintDescription {
1364    EntityConstraintDescription {
1365        id,
1366        name: name.to_string(),
1367        kind: String::new(),
1368        origin: accepted_constraint_origin_label(origin).to_string(),
1369        validation_state: "validated".to_string(),
1370        validation_progress: None,
1371        field_id: None,
1372        index_id: None,
1373        relation_id: None,
1374        fields: Vec::new(),
1375        index: None,
1376        predicate_sql: None,
1377        relation: None,
1378        target_entity: None,
1379        action: None,
1380        semantics: String::new(),
1381        check_sql: None,
1382    }
1383}
1384
1385fn apply_unique_index_description(
1386    description: &mut EntityConstraintDescription,
1387    index: &PersistedIndexSnapshot,
1388) {
1389    description.kind = "unique".to_string();
1390    description.index_id = Some(index.schema_id().get());
1391    description.fields = describe_persisted_index_fields(index.key());
1392    description.index = Some(index.name().to_string());
1393    description.predicate_sql = index.predicate_sql().map(str::to_string);
1394    description.semantics = if index.predicate_sql().is_some() {
1395        "partial_unique_index_v1"
1396    } else {
1397        "unique_index_v1"
1398    }
1399    .to_string();
1400}
1401
1402const fn accepted_constraint_origin_label(origin: ConstraintOrigin) -> &'static str {
1403    match origin {
1404        ConstraintOrigin::Generated => "generated",
1405        ConstraintOrigin::SqlDdl => "sql_ddl",
1406    }
1407}
1408
1409fn accepted_field_name(
1410    snapshot: &crate::db::schema::PersistedSchemaSnapshot,
1411    field_id: FieldId,
1412) -> Result<String, InternalError> {
1413    snapshot
1414        .fields()
1415        .iter()
1416        .find(|field| field.id() == field_id)
1417        .map(|field| field.name().to_string())
1418        .ok_or_else(InternalError::store_invariant)
1419}
1420
1421fn render_primary_key_fields(fields: &[String]) -> String {
1422    fields.join(", ")
1423}
1424
1425fn describe_entity_indexes_with_persisted_schema(
1426    schema: &AcceptedSchemaSnapshot,
1427) -> Vec<EntityIndexDescription> {
1428    schema
1429        .persisted_snapshot()
1430        .indexes()
1431        .iter()
1432        .map(|index| {
1433            EntityIndexDescription::new(
1434                index.name().to_string(),
1435                index.unique(),
1436                describe_persisted_index_fields(index.key()),
1437                if index.generated() {
1438                    "generated".to_string()
1439                } else {
1440                    "ddl".to_string()
1441                },
1442            )
1443        })
1444        .collect()
1445}
1446
1447fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
1448    match key {
1449        PersistedIndexKeySnapshot::FieldPath(paths) => paths
1450            .iter()
1451            .map(|field_path| field_path.path().join("."))
1452            .collect(),
1453        PersistedIndexKeySnapshot::Items(items) => items
1454            .iter()
1455            .map(|item| match item {
1456                PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
1457                PersistedIndexKeyItemSnapshot::Expression(expression) => {
1458                    expression.canonical_text().to_string()
1459                }
1460            })
1461            .collect(),
1462    }
1463}
1464
1465/// Build the canonical compact SQL column projection from accepted authority.
1466pub(in crate::db) fn describe_compact_columns_with_persisted_schema(
1467    schema: &AcceptedSchemaSnapshot,
1468    value_catalog: &AcceptedValueCatalogHandle,
1469) -> Result<Vec<SqlColumnSummary>, InternalError> {
1470    let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1471    let snapshot = schema.persisted_snapshot();
1472    if snapshot.fields().len() != row_layout.fields().len()
1473        || snapshot.fields().len() > icydb_schema::MAX_FRAGMENT_FIELDS
1474    {
1475        return Err(InternalError::store_invariant());
1476    }
1477
1478    let capacity = compact_column_capacity(snapshot.fields())?;
1479    let mut accepted_fields = snapshot
1480        .fields()
1481        .iter()
1482        .zip(row_layout.fields())
1483        .collect::<Vec<_>>();
1484    icydb_schema::compact_sort_unstable_by(&mut accepted_fields, |left, right| {
1485        left.0.id().cmp(&right.0.id())
1486    });
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        icydb_schema::compact_sort_unstable_by(&mut nested, |left, right| {
1518            left.path().cmp(right.path())
1519        });
1520        for leaf in nested {
1521            let mut canonical_path = Vec::with_capacity(leaf.path().len().saturating_add(1));
1522            canonical_path.push(field.name());
1523            canonical_path.extend(leaf.path().iter().map(String::as_str));
1524            let canonical_name = canonical_path.join(".");
1525            columns.push(SqlColumnSummary::new(
1526                canonical_name.clone(),
1527                summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1528                nested_path_nullable(field.nullable(), field.nested_leaves(), leaf.path()),
1529                compact_column_key(snapshot, canonical_name.as_str()),
1530                SqlColumnDefault::NotApplicable,
1531                compact_column_extras(false, generated, false),
1532            )?);
1533        }
1534    }
1535
1536    if columns.len() != capacity {
1537        return Err(InternalError::store_invariant());
1538    }
1539    Ok(columns)
1540}
1541
1542fn compact_column_capacity(
1543    fields: &[crate::db::schema::PersistedFieldSnapshot],
1544) -> Result<usize, InternalError> {
1545    compact_column_capacity_from_counts(
1546        fields.len(),
1547        fields.iter().map(|field| field.nested_leaves().len()),
1548    )
1549}
1550
1551fn compact_column_capacity_from_counts(
1552    field_count: usize,
1553    nested_counts: impl IntoIterator<Item = usize>,
1554) -> Result<usize, InternalError> {
1555    if field_count > icydb_schema::MAX_FRAGMENT_FIELDS {
1556        return Err(InternalError::store_invariant());
1557    }
1558    let mut seen_fields = 0usize;
1559    let mut total = field_count;
1560    for nested_count in nested_counts {
1561        seen_fields = seen_fields
1562            .checked_add(1)
1563            .ok_or_else(InternalError::store_invariant)?;
1564        if nested_count > icydb_schema::MAX_FRAGMENT_FIELDS {
1565            return Err(InternalError::store_invariant());
1566        }
1567        total = total
1568            .checked_add(nested_count)
1569            .ok_or_else(InternalError::store_invariant)?;
1570    }
1571    if seen_fields != field_count {
1572        return Err(InternalError::store_invariant());
1573    }
1574    if total > MAX_SQL_COMPACT_COLUMN_ROWS {
1575        return Err(InternalError::store_invariant());
1576    }
1577    Ok(total)
1578}
1579
1580const fn accepted_write_policy_generates(field: &AcceptedRowLayoutRuntimeField<'_>) -> bool {
1581    let policy = field.write_policy();
1582    policy.insert_generation().is_some() || policy.write_management().is_some()
1583}
1584
1585fn compact_column_extras(identity: bool, generated: bool, relation: bool) -> Vec<SqlColumnExtra> {
1586    let mut extra = Vec::with_capacity(MAX_SQL_COLUMN_EXTRA_FLAGS);
1587    if identity {
1588        extra.push(SqlColumnExtra::Identity);
1589    }
1590    if generated {
1591        extra.push(SqlColumnExtra::Generated);
1592    }
1593    if relation {
1594        extra.push(SqlColumnExtra::Relation);
1595    }
1596    extra
1597}
1598
1599fn compact_column_default(
1600    field: &AcceptedRowLayoutRuntimeField<'_>,
1601    value_catalog: &AcceptedValueCatalogHandle,
1602) -> Result<SqlColumnDefault, InternalError> {
1603    if accepted_write_policy_generates(field) {
1604        return Ok(SqlColumnDefault::Auto);
1605    }
1606    match field.insert_omission_policy() {
1607        AcceptedInsertOmissionPolicy::NullIfMissing => Ok(SqlColumnDefault::Null),
1608        AcceptedInsertOmissionPolicy::DefaultIfMissing => {
1609            let payload = field
1610                .insert_default()
1611                .slot_payload()
1612                .ok_or_else(InternalError::store_invariant)?;
1613            let rendered = accepted_payload_facts(field, value_catalog, payload)?;
1614            Ok(SqlColumnDefault::Literal {
1615                text: rendered.value,
1616            })
1617        }
1618        AcceptedInsertOmissionPolicy::Required => Ok(SqlColumnDefault::Required),
1619    }
1620}
1621
1622fn nested_path_nullable(
1623    top_level_nullable: bool,
1624    leaves: &[PersistedNestedLeafSnapshot],
1625    path: &[String],
1626) -> bool {
1627    top_level_nullable
1628        || leaves.iter().any(|candidate| {
1629            candidate.path().len() <= path.len()
1630                && path.starts_with(candidate.path())
1631                && candidate.nullable()
1632        })
1633}
1634
1635fn compact_column_key(snapshot: &PersistedSchemaSnapshot, path: &str) -> SqlColumnKey {
1636    let top_level_field = snapshot.fields().iter().find(|field| field.name() == path);
1637    let primary =
1638        top_level_field.is_some_and(|field| snapshot.primary_key_field_ids().contains(&field.id()));
1639    let memberships = snapshot.indexes().iter().filter_map(|index| {
1640        let key_items = match index.key() {
1641            PersistedIndexKeySnapshot::FieldPath(paths) => paths.len(),
1642            PersistedIndexKeySnapshot::Items(items) => items.len(),
1643        };
1644        let exact_path_member = match index.key() {
1645            PersistedIndexKeySnapshot::FieldPath(paths) => {
1646                paths.iter().any(|item| item.path().join(".") == path)
1647            }
1648            PersistedIndexKeySnapshot::Items(items) => items.iter().any(|item| {
1649                matches!(
1650                    item,
1651                    PersistedIndexKeyItemSnapshot::FieldPath(field_path)
1652                        if field_path.path().join(".") == path
1653                )
1654            }),
1655        };
1656        if !exact_path_member {
1657            return None;
1658        }
1659        Some((index.unique(), key_items))
1660    });
1661    classify_compact_column_key(primary, memberships)
1662}
1663
1664fn classify_compact_column_key(
1665    primary: bool,
1666    memberships: impl IntoIterator<Item = (bool, usize)>,
1667) -> SqlColumnKey {
1668    if primary {
1669        return SqlColumnKey::Primary;
1670    }
1671    let mut multiple = false;
1672    for (unique, key_items) in memberships {
1673        if unique && key_items == 1 {
1674            return SqlColumnKey::Unique;
1675        }
1676        multiple = true;
1677    }
1678    if multiple {
1679        SqlColumnKey::Multiple
1680    } else {
1681        SqlColumnKey::None
1682    }
1683}
1684
1685#[cfg_attr(
1686    doc,
1687    doc = "Build field descriptors using accepted persisted schema slot metadata."
1688)]
1689#[cfg(any(test, feature = "sql"))]
1690pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1691    schema: &AcceptedSchemaSnapshot,
1692    value_catalog: &AcceptedValueCatalogHandle,
1693) -> Result<Vec<EntityFieldDescription>, InternalError> {
1694    let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1695    describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1696}
1697
1698fn describe_entity_fields_with_runtime_contract(
1699    schema: &AcceptedSchemaSnapshot,
1700    row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1701    value_catalog: &AcceptedValueCatalogHandle,
1702) -> Result<Vec<EntityFieldDescription>, InternalError> {
1703    let snapshot = schema.persisted_snapshot();
1704    if snapshot.fields().len() != row_layout.fields().len() {
1705        return Err(InternalError::store_invariant());
1706    }
1707    let mut fields = Vec::with_capacity(snapshot.fields().len());
1708
1709    // Accepted-schema describe surfaces must follow the stored schema payload,
1710    // not the generated model's current field order.
1711    for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1712        if field.id() != runtime_field.field_id() {
1713            return Err(InternalError::store_invariant());
1714        }
1715        let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1716        let slot = Some(runtime_field.slot().get());
1717        let metadata = DescribeFieldMetadata::new(
1718            summarize_persisted_field_kind(field.kind(), value_catalog)?,
1719            field.nullable(),
1720            field_type_from_persisted_kind(&query_field_kind_from_persisted_kind(
1721                field.kind(),
1722                value_catalog.composite_catalog(),
1723            ))
1724            .is_queryable(),
1725            field_origin_label(field.generated()),
1726        );
1727        let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1728
1729        push_described_field_row(
1730            &mut fields,
1731            field.name(),
1732            slot,
1733            primary_key,
1734            None,
1735            metadata,
1736            temporal,
1737        );
1738
1739        if !field.nested_leaves().is_empty() {
1740            describe_persisted_nested_leaves(
1741                &mut fields,
1742                field.nested_leaves(),
1743                field_origin_label(field.generated()),
1744                value_catalog,
1745            )?;
1746        }
1747    }
1748
1749    Ok(fields)
1750}
1751
1752///
1753/// DescribeFieldMetadata
1754///
1755/// Field-description metadata selected before one field row is rendered.
1756///
1757
1758struct DescribeFieldMetadata {
1759    kind: String,
1760    nullable: bool,
1761    queryable: bool,
1762    origin: String,
1763}
1764
1765impl DescribeFieldMetadata {
1766    // Build one metadata bundle from already-rendered field facts.
1767    const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1768        Self {
1769            kind,
1770            nullable,
1771            queryable,
1772            origin,
1773        }
1774    }
1775}
1776
1777// Add one already-resolved field row to the stable describe DTO list. The
1778// caller owns where metadata came from: generated model or accepted schema.
1779fn push_described_field_row(
1780    fields: &mut Vec<EntityFieldDescription>,
1781    name: &str,
1782    slot: Option<u16>,
1783    primary_key: bool,
1784    tree_prefix: Option<&'static str>,
1785    metadata: DescribeFieldMetadata,
1786    temporal: EntityFieldTemporalFacts,
1787) {
1788    // Nested field rows keep a compact tree marker so table-oriented describe
1789    // output scans as a hierarchy without assigning nested leaves row slots.
1790    let display_name = if let Some(prefix) = tree_prefix {
1791        format!("{prefix}{name}")
1792    } else {
1793        name.to_string()
1794    };
1795
1796    fields.push(EntityFieldDescription::new_with_temporal_facts(
1797        display_name,
1798        slot,
1799        primary_key,
1800        metadata,
1801        temporal,
1802    ));
1803}
1804
1805// Render accepted nested leaf descriptors. Nested leaves do not own physical
1806// row slots, so they always appear with the no-slot sentinel in the Candid DTO.
1807fn describe_persisted_nested_leaves(
1808    fields: &mut Vec<EntityFieldDescription>,
1809    nested_leaves: &[PersistedNestedLeafSnapshot],
1810    origin: String,
1811    value_catalog: &AcceptedValueCatalogHandle,
1812) -> Result<(), InternalError> {
1813    for (index, leaf) in nested_leaves.iter().enumerate() {
1814        let prefix = if index + 1 == nested_leaves.len() {
1815            "└─ "
1816        } else {
1817            "├─ "
1818        };
1819        let name = leaf.path().last().map_or("", String::as_str);
1820        let metadata = DescribeFieldMetadata::new(
1821            summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1822            leaf.nullable(),
1823            field_type_from_persisted_kind(&query_field_kind_from_persisted_kind(
1824                leaf.kind(),
1825                value_catalog.composite_catalog(),
1826            ))
1827            .is_queryable(),
1828            origin.clone(),
1829        );
1830
1831        push_described_field_row(
1832            fields,
1833            name,
1834            None,
1835            false,
1836            Some(prefix),
1837            metadata,
1838            EntityFieldTemporalFacts::nested(),
1839        );
1840    }
1841
1842    Ok(())
1843}
1844
1845fn field_origin_label(generated: bool) -> String {
1846    if generated {
1847        "generated".to_string()
1848    } else {
1849        "ddl".to_string()
1850    }
1851}
1852
1853pub(in crate::db) fn describe_entity_relations_with_persisted_schema(
1854    schema: &AcceptedSchemaSnapshot,
1855    resolve_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
1856) -> Result<Vec<EntityRelationDescription>, InternalError> {
1857    let snapshot = schema.persisted_snapshot();
1858    if snapshot.relations().len() > icydb_schema::MAX_FRAGMENT_RELATIONS {
1859        return Err(InternalError::store_invariant());
1860    }
1861    snapshot
1862        .relations()
1863        .iter()
1864        .map(|relation| {
1865            let local_fields = relation
1866                .local_field_ids()
1867                .iter()
1868                .map(|field_id| accepted_field_name(snapshot, *field_id))
1869                .collect::<Result<Vec<_>, _>>()?;
1870            let (target_entity_name, target_store_path) = resolve_target(relation.target_path())?;
1871
1872            Ok(EntityRelationDescription::new(
1873                render_primary_key_fields(local_fields.as_slice()),
1874                relation.target_path().to_string(),
1875                target_entity_name,
1876                target_store_path,
1877                persisted_relation_cardinality(snapshot, relation)?,
1878            ))
1879        })
1880        .collect()
1881}
1882
1883fn persisted_relation_cardinality(
1884    snapshot: &PersistedSchemaSnapshot,
1885    relation: &PersistedRelationEdgeSnapshot,
1886) -> Result<EntityRelationCardinality, InternalError> {
1887    let [field_id] = relation.local_field_ids() else {
1888        return Ok(EntityRelationCardinality::Single);
1889    };
1890    let field = snapshot
1891        .fields()
1892        .iter()
1893        .find(|field| field.id() == *field_id)
1894        .ok_or_else(InternalError::store_invariant)?;
1895
1896    Ok(match field.kind() {
1897        AcceptedFieldKind::List(_) => EntityRelationCardinality::List,
1898        AcceptedFieldKind::Set(_) => EntityRelationCardinality::Set,
1899        _ => EntityRelationCardinality::Single,
1900    })
1901}
1902
1903fn write_accepted_composite_shape_summary(
1904    out: &mut String,
1905    shape: &AcceptedCompositeShape,
1906    value_catalog: &AcceptedValueCatalogHandle,
1907) -> Result<(), InternalError> {
1908    match shape {
1909        AcceptedCompositeShape::Record(fields) => {
1910            out.push_str("record{");
1911            for (index, field) in fields.iter().enumerate() {
1912                if index > 0 {
1913                    out.push_str(", ");
1914                }
1915                out.push_str(field.name());
1916                out.push(':');
1917                write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1918            }
1919            out.push('}');
1920        }
1921        AcceptedCompositeShape::Tuple(elements) => {
1922            out.push_str("tuple<");
1923            for (index, element) in elements.iter().enumerate() {
1924                if index > 0 {
1925                    out.push_str(", ");
1926                }
1927                write_accepted_composite_element_summary(out, element, value_catalog)?;
1928            }
1929            out.push('>');
1930        }
1931        AcceptedCompositeShape::Newtype(inner) => {
1932            out.push_str("newtype<");
1933            write_accepted_composite_element_summary(out, inner, value_catalog)?;
1934            out.push('>');
1935        }
1936    }
1937
1938    Ok(())
1939}
1940
1941fn write_accepted_composite_element_summary(
1942    out: &mut String,
1943    element: &AcceptedCompositeElement,
1944    value_catalog: &AcceptedValueCatalogHandle,
1945) -> Result<(), InternalError> {
1946    write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1947    write_composite_nullability_summary(out, element.nullable());
1948    Ok(())
1949}
1950
1951fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1952    match codec {
1953        CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1954    }
1955}
1956
1957fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1958    if nullable {
1959        out.push('?');
1960    }
1961}
1962
1963// Write the common text/blob describe label. Both generated and accepted schema
1964// summaries use this path so bounded and explicitly unbounded contracts stay
1965// visibly identical across `DESCRIBE` and `SHOW COLUMNS`.
1966fn write_length_bounded_field_kind_summary(
1967    out: &mut String,
1968    kind_name: &str,
1969    max_len: Option<u32>,
1970) {
1971    out.push_str(kind_name);
1972    if let Some(max_len) = max_len {
1973        out.push_str("(max_len=");
1974        out.push_str(&max_len.to_string());
1975        out.push(')');
1976    } else {
1977        out.push_str("(unbounded)");
1978    }
1979}
1980
1981fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1982    out.push_str(kind_name);
1983    out.push_str("(max_bytes=");
1984    out.push_str(&max_bytes.to_string());
1985    out.push(')');
1986}
1987
1988///
1989/// RenderedTemporalPayload
1990///
1991/// One accepted temporal payload projected as an inseparable bounded value,
1992/// byte count, and stable diagnostic hash.
1993///
1994
1995struct RenderedTemporalPayload {
1996    value: String,
1997    bytes: u32,
1998    hash: String,
1999}
2000
2001fn accepted_field_temporal_facts(
2002    field: &AcceptedRowLayoutRuntimeField<'_>,
2003    value_catalog: &AcceptedValueCatalogHandle,
2004) -> Result<EntityFieldTemporalFacts, InternalError> {
2005    let write_policy = field.write_policy();
2006    let insert_omission = if write_policy.insert_generation().is_some() {
2007        "generated"
2008    } else if write_policy.write_management().is_some() {
2009        "managed"
2010    } else {
2011        match field.insert_omission_policy() {
2012            AcceptedInsertOmissionPolicy::NullIfMissing => "null",
2013            AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
2014            AcceptedInsertOmissionPolicy::Required => "required",
2015        }
2016    };
2017    let insert_default = field
2018        .insert_default()
2019        .slot_payload()
2020        .map(|payload| accepted_payload_facts(field, value_catalog, payload))
2021        .transpose()?;
2022    let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
2023        Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
2024        None => (None, None, None),
2025    };
2026    let (historical_fill, historical_fill_bytes, historical_fill_hash) =
2027        match field.historical_fill() {
2028            SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
2029            SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
2030            SchemaHistoricalFill::SlotPayload(payload) => {
2031                let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
2032                (
2033                    Some(rendered.value),
2034                    Some(rendered.bytes),
2035                    Some(rendered.hash),
2036                )
2037            }
2038        };
2039
2040    Ok(EntityFieldTemporalFacts {
2041        insert_omission: Some(insert_omission.to_string()),
2042        insert_default,
2043        insert_default_bytes,
2044        insert_default_hash,
2045        introduced_in_layout: Some(field.introduced_in_layout().get()),
2046        historical_fill,
2047        historical_fill_bytes,
2048        historical_fill_hash,
2049    })
2050}
2051
2052fn accepted_payload_facts(
2053    field: &AcceptedRowLayoutRuntimeField<'_>,
2054    value_catalog: &AcceptedValueCatalogHandle,
2055    payload: &[u8],
2056) -> Result<RenderedTemporalPayload, InternalError> {
2057    let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
2058        .map_err(|_| InternalError::store_invariant())?;
2059    let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
2060    let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.value())
2061        .map_err(|_| InternalError::store_invariant())?;
2062    let hash = short_default_payload_fingerprint(payload);
2063    let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
2064    let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
2065
2066    Ok(RenderedTemporalPayload {
2067        value: rendered,
2068        bytes,
2069        hash,
2070    })
2071}
2072
2073fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
2074    let rendered = match value {
2075        OutputValue::Text(value) => format!("'{}'", value.escape_default()),
2076        _ => render_output_value_text(value),
2077    };
2078    if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
2079        return rendered;
2080    }
2081
2082    format!(
2083        "{}(bytes={}, sha256={})",
2084        output_value_kind_label(value),
2085        payload.len(),
2086        hash,
2087    )
2088}
2089
2090const fn output_value_kind_label(value: &OutputValue) -> &'static str {
2091    match value {
2092        OutputValue::Account(_) => "account",
2093        OutputValue::Blob(_) => "blob",
2094        OutputValue::Bool(_) => "bool",
2095        OutputValue::Date(_) => "date",
2096        OutputValue::Decimal(_) => "decimal",
2097        OutputValue::Duration(_) => "duration",
2098        OutputValue::Enum(_) => "enum",
2099        OutputValue::Float32(_) => "float32",
2100        OutputValue::Float64(_) => "float64",
2101        OutputValue::Int64(_) => "int64",
2102        OutputValue::Int128(_) => "int128",
2103        OutputValue::IntBig(_) => "int_big",
2104        OutputValue::List(_) => "list",
2105        OutputValue::Map(_) => "map",
2106        OutputValue::Null => "null",
2107        OutputValue::Principal(_) => "principal",
2108        OutputValue::Subaccount(_) => "subaccount",
2109        OutputValue::Text(_) => "text",
2110        OutputValue::Timestamp(_) => "timestamp",
2111        OutputValue::Nat64(_) => "nat64",
2112        OutputValue::Nat128(_) => "nat128",
2113        OutputValue::NatBig(_) => "nat_big",
2114        OutputValue::Ulid(_) => "ulid",
2115        OutputValue::Unit => "unit",
2116    }
2117}
2118
2119fn short_default_payload_fingerprint(payload: &[u8]) -> String {
2120    let digest = Sha256::digest(payload);
2121    let mut out = String::with_capacity(16);
2122    for byte in &digest[..8] {
2123        let _ = write!(out, "{byte:02x}");
2124    }
2125    out
2126}
2127
2128#[cfg_attr(
2129    doc,
2130    doc = "Render one stable field-kind label from accepted persisted schema metadata."
2131)]
2132fn summarize_persisted_field_kind(
2133    kind: &AcceptedFieldKind,
2134    value_catalog: &AcceptedValueCatalogHandle,
2135) -> Result<String, InternalError> {
2136    let mut out = String::new();
2137    write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
2138
2139    Ok(out)
2140}
2141
2142// Stream the accepted persisted field-kind label in the stable public
2143// `DESCRIBE` format directly from live schema metadata.
2144fn write_persisted_field_kind_summary(
2145    out: &mut String,
2146    kind: &AcceptedFieldKind,
2147    value_catalog: &AcceptedValueCatalogHandle,
2148) -> Result<(), InternalError> {
2149    if let Some(name) = describe_kind_name(kind) {
2150        out.push_str(name);
2151        return Ok(());
2152    }
2153
2154    match kind {
2155        AcceptedFieldKind::Blob { max_len } => {
2156            write_length_bounded_field_kind_summary(out, "blob", *max_len);
2157        }
2158        AcceptedFieldKind::Decimal { scale } => {
2159            let _ = write!(out, "decimal(scale={scale})");
2160        }
2161        AcceptedFieldKind::IntBig { max_bytes } => {
2162            write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
2163        }
2164        AcceptedFieldKind::Enum { type_id } => {
2165            let definition = value_catalog
2166                .enum_catalog()
2167                .enum_type(*type_id)
2168                .ok_or_else(InternalError::store_invariant)?;
2169            out.push_str("enum(");
2170            out.push_str(definition.path());
2171            out.push(')');
2172        }
2173        AcceptedFieldKind::Text { max_len } => {
2174            write_length_bounded_field_kind_summary(out, "text", *max_len);
2175        }
2176        AcceptedFieldKind::Relation {
2177            target_entity_name,
2178            key_kind,
2179            ..
2180        } => {
2181            out.push_str("relation(target=");
2182            out.push_str(target_entity_name);
2183            out.push_str(", key=");
2184            write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
2185            out.push(')');
2186        }
2187        AcceptedFieldKind::List(inner) => {
2188            out.push_str("list<");
2189            write_persisted_field_kind_summary(out, inner, value_catalog)?;
2190            out.push('>');
2191        }
2192        AcceptedFieldKind::Set(inner) => {
2193            out.push_str("set<");
2194            write_persisted_field_kind_summary(out, inner, value_catalog)?;
2195            out.push('>');
2196        }
2197        AcceptedFieldKind::Map { key, value } => {
2198            out.push_str("map<");
2199            write_persisted_field_kind_summary(out, key, value_catalog)?;
2200            out.push_str(", ");
2201            write_persisted_field_kind_summary(out, value, value_catalog)?;
2202            out.push('>');
2203        }
2204        AcceptedFieldKind::Composite { type_id } => {
2205            let composite_catalog = value_catalog.composite_catalog();
2206            let definition = composite_catalog
2207                .composite_type(*type_id)
2208                .ok_or_else(InternalError::store_invariant)?;
2209            out.push_str("composite(path=");
2210            out.push_str(definition.path());
2211            out.push_str(", codec=");
2212            write_composite_codec_summary(out, definition.codec());
2213            out.push_str(", shape=");
2214            write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
2215            out.push(')');
2216        }
2217        AcceptedFieldKind::Account
2218        | AcceptedFieldKind::Bool
2219        | AcceptedFieldKind::Date
2220        | AcceptedFieldKind::Duration
2221        | AcceptedFieldKind::Float32
2222        | AcceptedFieldKind::Float64
2223        | AcceptedFieldKind::Int8
2224        | AcceptedFieldKind::Int16
2225        | AcceptedFieldKind::Int32
2226        | AcceptedFieldKind::Int64
2227        | AcceptedFieldKind::Int128
2228        | AcceptedFieldKind::Principal
2229        | AcceptedFieldKind::Subaccount
2230        | AcceptedFieldKind::Timestamp
2231        | AcceptedFieldKind::Nat8
2232        | AcceptedFieldKind::Nat16
2233        | AcceptedFieldKind::Nat32
2234        | AcceptedFieldKind::Nat64
2235        | AcceptedFieldKind::Nat128
2236        | AcceptedFieldKind::Ulid
2237        | AcceptedFieldKind::Unit => return Err(InternalError::store_invariant()),
2238        AcceptedFieldKind::NatBig { max_bytes } => {
2239            write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
2240        }
2241    }
2242
2243    Ok(())
2244}
2245
2246const fn describe_kind_name(kind: &AcceptedFieldKind) -> Option<&'static str> {
2247    Some(match kind {
2248        AcceptedFieldKind::Account => "account",
2249        AcceptedFieldKind::Bool => "bool",
2250        AcceptedFieldKind::Date => "date",
2251        AcceptedFieldKind::Duration => "duration",
2252        AcceptedFieldKind::Float32 => "float32",
2253        AcceptedFieldKind::Float64 => "float64",
2254        AcceptedFieldKind::Int8 => "int8",
2255        AcceptedFieldKind::Int16 => "int16",
2256        AcceptedFieldKind::Int32 => "int32",
2257        AcceptedFieldKind::Int64 => "int64",
2258        AcceptedFieldKind::Int128 => "int128",
2259        AcceptedFieldKind::Principal => "principal",
2260        AcceptedFieldKind::Subaccount => "subaccount",
2261        AcceptedFieldKind::Timestamp => "timestamp",
2262        AcceptedFieldKind::Nat8 => "nat8",
2263        AcceptedFieldKind::Nat16 => "nat16",
2264        AcceptedFieldKind::Nat32 => "nat32",
2265        AcceptedFieldKind::Nat64 => "nat64",
2266        AcceptedFieldKind::Nat128 => "nat128",
2267        AcceptedFieldKind::Ulid => "ulid",
2268        AcceptedFieldKind::Unit => "unit",
2269        AcceptedFieldKind::Blob { .. }
2270        | AcceptedFieldKind::Decimal { .. }
2271        | AcceptedFieldKind::Enum { .. }
2272        | AcceptedFieldKind::IntBig { .. }
2273        | AcceptedFieldKind::NatBig { .. }
2274        | AcceptedFieldKind::Text { .. }
2275        | AcceptedFieldKind::Relation { .. }
2276        | AcceptedFieldKind::List(_)
2277        | AcceptedFieldKind::Set(_)
2278        | AcceptedFieldKind::Map { .. }
2279        | AcceptedFieldKind::Composite { .. } => return None,
2280    })
2281}
2282
2283//
2284// TESTS
2285//
2286
2287#[cfg(test)]
2288mod tests {
2289    use std::collections::BTreeMap;
2290
2291    use super::{
2292        EntityIdentityDescription, EntityRelationCardinality, EntityRelationDescription,
2293        MAX_SCHEMA_VALUE_RENDER_CHARS, SqlColumnDefault, SqlColumnExtra, SqlColumnKey,
2294        SqlColumnSummary, SqlDescribeOutput, SqlShowRelationsOutput, classify_compact_column_key,
2295        compact_column_capacity_from_counts, compact_column_extras, describe_accepted_constraint,
2296        describe_compact_columns_with_persisted_schema, nested_path_nullable,
2297    };
2298    use crate::db::schema::{
2299        AcceptedCompositeCatalog, AcceptedConstraintCatalog, AcceptedFieldKind,
2300        AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
2301        CompositeFieldId, CompositeTypeId, FieldId, FieldStorageDecode, LeafCodec,
2302        MAX_SCHEMA_SNAPSHOT_BYTES, PersistedFieldSnapshot, PersistedIndexFieldPathSnapshot,
2303        PersistedIndexKeySnapshot, PersistedIndexSnapshot, PersistedNestedLeafSnapshot,
2304        PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault,
2305        SchemaRowLayout, SchemaVersion,
2306        composite_catalog::{
2307            AcceptedCompositeElement, AcceptedCompositeField, AcceptedCompositeShape,
2308            decode_accepted_composite_catalog, encode_accepted_composite_catalog,
2309        },
2310        decode_persisted_schema_snapshot, empty_accepted_enum_catalog_for_tests,
2311        encode_persisted_schema_snapshot,
2312    };
2313
2314    use candid::Encode;
2315
2316    const REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS: usize = icydb_schema::MAX_FRAGMENT_FIELDS;
2317    const REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES: usize = 95;
2318    const IC_QUERY_REPLY_BYTES: usize = 3 * 1024 * 1024;
2319
2320    #[test]
2321    fn filtered_unique_constraint_description_exposes_partial_backing_contract() {
2322        let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2323            SchemaVersion::initial(),
2324            "tests::Account".to_string(),
2325            "Account".to_string(),
2326            FieldId::new(1),
2327            SchemaRowLayout::initial(vec![
2328                (FieldId::new(1), SchemaFieldSlot::new(0)),
2329                (FieldId::new(2), SchemaFieldSlot::new(1)),
2330            ]),
2331            vec![
2332                PersistedFieldSnapshot::new_initial(
2333                    FieldId::new(1),
2334                    "id".to_string(),
2335                    SchemaFieldSlot::new(0),
2336                    AcceptedFieldKind::Ulid,
2337                    Vec::new(),
2338                    false,
2339                    SchemaInsertDefault::None,
2340                    FieldStorageDecode::ByKind,
2341                    LeafCodec::Scalar(ScalarCodec::Ulid),
2342                ),
2343                PersistedFieldSnapshot::new_initial(
2344                    FieldId::new(2),
2345                    "email".to_string(),
2346                    SchemaFieldSlot::new(1),
2347                    AcceptedFieldKind::Text { max_len: None },
2348                    Vec::new(),
2349                    true,
2350                    SchemaInsertDefault::None,
2351                    FieldStorageDecode::ByKind,
2352                    LeafCodec::Scalar(ScalarCodec::Text),
2353                ),
2354            ],
2355            vec![PersistedIndexSnapshot::new(
2356                SchemaIndexId::new(1).expect("test index identity should be non-zero"),
2357                1,
2358                "account_email".to_string(),
2359                "tests::Account::account_email".to_string(),
2360                true,
2361                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2362                    FieldId::new(2),
2363                    SchemaFieldSlot::new(1),
2364                    vec!["email".to_string()],
2365                    AcceptedFieldKind::Text { max_len: None },
2366                    true,
2367                )]),
2368                Some("email IS NOT NULL".to_string()),
2369            )],
2370        );
2371        let catalog = AcceptedConstraintCatalog::initial(
2372            snapshot.fields(),
2373            snapshot.indexes(),
2374            snapshot.relations(),
2375        )
2376        .expect("fixture constraints should build");
2377        let snapshot = snapshot.with_constraint_catalog(catalog);
2378        let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
2379            empty_accepted_enum_catalog_for_tests(),
2380            AcceptedCompositeCatalog::empty(),
2381            AcceptedSchemaRevision::INITIAL,
2382        );
2383        let constraint = snapshot
2384            .constraints()
2385            .iter()
2386            .find(|constraint| constraint.name() == "account_email")
2387            .expect("unique constraint should exist");
2388
2389        let description = describe_accepted_constraint(&snapshot, &value_catalog, constraint)
2390            .expect("accepted unique constraint should describe");
2391        assert_eq!(description.index_id(), Some(1));
2392        assert_eq!(description.index(), Some("account_email"));
2393        assert_eq!(description.predicate_sql(), Some("email IS NOT NULL"));
2394        assert_eq!(description.semantics(), "partial_unique_index_v1");
2395    }
2396
2397    #[test]
2398    fn identity_description_reports_exact_remaining_capacity_and_exhaustion() {
2399        let available =
2400            EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 254)
2401                .expect("in-domain Identity description should build");
2402        assert_eq!(available.minimum(), 1);
2403        assert_eq!(available.maximum(), 255);
2404        assert_eq!(available.high_water(), 254);
2405        assert_eq!(available.remaining(), 1);
2406        assert!(!available.exhausted());
2407
2408        let exhausted =
2409            EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 255)
2410                .expect("exact-domain exhaustion should remain describable");
2411        assert_eq!(exhausted.remaining(), 0);
2412        assert!(exhausted.exhausted());
2413
2414        assert!(
2415            EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 256).is_err(),
2416            "state beyond the accepted domain must not be described",
2417        );
2418    }
2419
2420    #[test]
2421    fn compact_key_contract_distinguishes_single_unique_from_compound_membership() {
2422        assert_eq!(
2423            classify_compact_column_key(true, [(true, 1), (false, 2)]),
2424            SqlColumnKey::Primary
2425        );
2426        assert_eq!(
2427            classify_compact_column_key(false, [(true, 2)]),
2428            SqlColumnKey::Multiple,
2429            "compound unique membership must not imply independent uniqueness",
2430        );
2431        assert_eq!(
2432            classify_compact_column_key(false, [(false, 1), (true, 1)]),
2433            SqlColumnKey::Unique,
2434            "single-field unique membership has precedence over non-unique membership",
2435        );
2436        assert_eq!(
2437            classify_compact_column_key(false, std::iter::empty()),
2438            SqlColumnKey::None
2439        );
2440    }
2441
2442    #[test]
2443    fn compact_extra_contract_is_closed_and_deterministically_ordered() {
2444        assert_eq!(
2445            compact_column_extras(true, true, true),
2446            vec![
2447                SqlColumnExtra::Identity,
2448                SqlColumnExtra::Generated,
2449                SqlColumnExtra::Relation,
2450            ]
2451        );
2452        assert_eq!(
2453            compact_column_extras(false, true, false),
2454            vec![SqlColumnExtra::Generated]
2455        );
2456        assert!(compact_column_extras(false, false, false).is_empty());
2457    }
2458
2459    #[test]
2460    fn compact_projection_bounds_accept_maximum_and_reject_max_plus_one() {
2461        assert_eq!(
2462            compact_column_capacity_from_counts(
2463                icydb_schema::MAX_FRAGMENT_FIELDS,
2464                std::iter::repeat_n(
2465                    icydb_schema::MAX_FRAGMENT_FIELDS,
2466                    icydb_schema::MAX_FRAGMENT_FIELDS,
2467                ),
2468            )
2469            .expect("accepted maximum should remain projectable"),
2470            super::MAX_SQL_COMPACT_COLUMN_ROWS,
2471        );
2472        assert!(
2473            compact_column_capacity_from_counts(
2474                icydb_schema::MAX_FRAGMENT_FIELDS + 1,
2475                std::iter::repeat_n(0, icydb_schema::MAX_FRAGMENT_FIELDS + 1),
2476            )
2477            .is_err()
2478        );
2479        assert!(
2480            compact_column_capacity_from_counts(1, [icydb_schema::MAX_FRAGMENT_FIELDS + 1],)
2481                .is_err()
2482        );
2483
2484        let valid = SqlColumnSummary::new(
2485            "value".to_string(),
2486            "text".to_string(),
2487            false,
2488            SqlColumnKey::None,
2489            SqlColumnDefault::Literal {
2490                text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS),
2491            },
2492            vec![
2493                SqlColumnExtra::Identity,
2494                SqlColumnExtra::Generated,
2495                SqlColumnExtra::Relation,
2496            ],
2497        );
2498        let valid = valid.expect("the complete admitted compact row should remain valid");
2499        assert!(
2500            SqlColumnSummary::new(
2501                "value".to_string(),
2502                "text".to_string(),
2503                false,
2504                SqlColumnKey::None,
2505                SqlColumnDefault::Literal {
2506                    text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS + 1),
2507                },
2508                Vec::new(),
2509            )
2510            .is_err()
2511        );
2512        assert!(
2513            SqlColumnSummary::new(
2514                "value".to_string(),
2515                "text".to_string(),
2516                false,
2517                SqlColumnKey::None,
2518                SqlColumnDefault::Required,
2519                vec![SqlColumnExtra::Generated; 4],
2520            )
2521            .is_err()
2522        );
2523
2524        let maximum = SqlDescribeOutput::Compact {
2525            entity: "AcceptedMaximum".to_string(),
2526            columns: vec![valid; super::MAX_SQL_COMPACT_COLUMN_ROWS],
2527        };
2528        let first = Encode!(&maximum).expect("accepted maximum should encode to bounded Candid");
2529        let second = Encode!(&maximum).expect("accepted maximum should encode deterministically");
2530        assert_eq!(first, second);
2531        assert_eq!(first.len(), 9_737_853);
2532
2533        let relation = EntityRelationDescription::new(
2534            "owner_id".to_string(),
2535            "entities::Owner".to_string(),
2536            "Owner".to_string(),
2537            "stores::Owner".to_string(),
2538            EntityRelationCardinality::Single,
2539        );
2540        assert!(
2541            SqlShowRelationsOutput::new(
2542                "Entry".to_string(),
2543                vec![relation.clone(); icydb_schema::MAX_FRAGMENT_RELATIONS],
2544            )
2545            .is_ok()
2546        );
2547        assert!(
2548            SqlShowRelationsOutput::new(
2549                "Entry".to_string(),
2550                vec![relation; icydb_schema::MAX_FRAGMENT_RELATIONS + 1],
2551            )
2552            .is_err()
2553        );
2554    }
2555
2556    #[test]
2557    fn reachable_accepted_compact_projection_exceeds_the_public_query_reply_limit() {
2558        let accepted = reachable_compact_reply_schema();
2559        let value_catalog = reachable_compact_reply_value_catalog();
2560        let columns = describe_compact_columns_with_persisted_schema(&accepted, &value_catalog)
2561            .expect("reachable accepted schema should project compact columns");
2562
2563        assert_eq!(
2564            columns.len(),
2565            1 + REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES
2566                * (1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS),
2567        );
2568        let output = SqlDescribeOutput::Compact {
2569            entity: accepted.entity_name().to_string(),
2570            columns,
2571        };
2572        let encoded_output =
2573            Encode!(&output).expect("reachable accepted compact output should encode");
2574        assert_eq!(encoded_output.len(), 3_693_116);
2575        assert!(
2576            encoded_output.len() > IC_QUERY_REPLY_BYTES,
2577            "a valid accepted schema must exercise the generated endpoint reply guard",
2578        );
2579    }
2580
2581    #[test]
2582    fn nested_nullability_includes_nullable_ancestors() {
2583        let leaves = vec![
2584            PersistedNestedLeafSnapshot::new(
2585                vec!["address".to_string()],
2586                AcceptedFieldKind::Unit,
2587                true,
2588            ),
2589            PersistedNestedLeafSnapshot::new(
2590                vec!["address".to_string(), "city".to_string()],
2591                AcceptedFieldKind::Unit,
2592                false,
2593            ),
2594        ];
2595        assert!(nested_path_nullable(
2596            false,
2597            leaves.as_slice(),
2598            &["address".to_string(), "city".to_string()],
2599        ));
2600        assert!(nested_path_nullable(
2601            true,
2602            leaves.as_slice(),
2603            &["other".to_string()],
2604        ));
2605        assert!(!nested_path_nullable(
2606            false,
2607            leaves.as_slice(),
2608            &["other".to_string()],
2609        ));
2610    }
2611
2612    fn compact_reply_leaf_name(index: usize) -> String {
2613        let first = u8::try_from(index / 26).expect("bounded leaf prefix fits u8") + b'a';
2614        let second = u8::try_from(index % 26).expect("bounded leaf suffix fits u8") + b'a';
2615        String::from_utf8(vec![first, second]).expect("ASCII leaf name should be UTF-8")
2616    }
2617
2618    fn compact_reply_top_level_name(index: usize) -> String {
2619        let prefix = format!("field_{index:03}_");
2620        format!("{prefix}{}", "x".repeat(128 - prefix.len()))
2621    }
2622
2623    fn reachable_compact_reply_schema() -> AcceptedSchemaSnapshot {
2624        let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
2625        let nested_leaves = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
2626            .map(|index| {
2627                PersistedNestedLeafSnapshot::new(
2628                    vec![compact_reply_leaf_name(index)],
2629                    AcceptedFieldKind::Unit,
2630                    false,
2631                )
2632            })
2633            .collect::<Vec<_>>();
2634        let mut fields = vec![PersistedFieldSnapshot::new_initial(
2635            FieldId::new(1),
2636            "id".to_string(),
2637            SchemaFieldSlot::new(0),
2638            AcceptedFieldKind::Nat64,
2639            Vec::new(),
2640            false,
2641            SchemaInsertDefault::None,
2642            FieldStorageDecode::ByKind,
2643            LeafCodec::Scalar(ScalarCodec::Nat64),
2644        )];
2645        let mut layout = vec![(FieldId::new(1), SchemaFieldSlot::new(0))];
2646        for index in 0..REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES {
2647            let raw_id = u32::try_from(index)
2648                .expect("bounded top-level index fits u32")
2649                .checked_add(2)
2650                .expect("bounded top-level identity has a successor");
2651            let raw_slot = u16::try_from(index)
2652                .expect("bounded top-level index fits u16")
2653                .checked_add(1)
2654                .expect("bounded top-level slot has a successor");
2655            let id = FieldId::new(raw_id);
2656            let slot = SchemaFieldSlot::new(raw_slot);
2657            fields.push(PersistedFieldSnapshot::new_initial(
2658                id,
2659                compact_reply_top_level_name(index),
2660                slot,
2661                AcceptedFieldKind::Composite {
2662                    type_id: composite_type_id,
2663                },
2664                nested_leaves.clone(),
2665                false,
2666                SchemaInsertDefault::None,
2667                FieldStorageDecode::ByKind,
2668                LeafCodec::Structural,
2669            ));
2670            layout.push((id, slot));
2671        }
2672        let persisted = PersistedSchemaSnapshot::new(
2673            SchemaVersion::initial(),
2674            "tests::ReachableCompactReply".to_string(),
2675            "ReachableCompactReply".to_string(),
2676            FieldId::new(1),
2677            SchemaRowLayout::initial(layout),
2678            fields,
2679        );
2680        let encoded = encode_persisted_schema_snapshot(&persisted)
2681            .expect("reachable compact-reply schema should fit its persisted payload limit");
2682        assert_eq!(encoded.len(), 310_861);
2683        assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
2684        AcceptedSchemaSnapshot::try_new(
2685            decode_persisted_schema_snapshot(encoded.as_slice())
2686                .expect("persisted compact-reply schema should decode"),
2687        )
2688        .expect("decoded compact-reply schema should satisfy accepted integrity")
2689    }
2690
2691    fn reachable_compact_reply_value_catalog() -> AcceptedValueCatalogHandle {
2692        let enum_catalog = empty_accepted_enum_catalog_for_tests();
2693        let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
2694        let composite_fields = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
2695            .map(|index| {
2696                let raw_id = u32::try_from(index)
2697                    .expect("bounded composite index fits u32")
2698                    .checked_add(1)
2699                    .expect("bounded composite identity has a successor");
2700                AcceptedCompositeField::new(
2701                    CompositeFieldId::new(raw_id).expect("composite field identity is non-zero"),
2702                    compact_reply_leaf_name(index),
2703                    AcceptedCompositeElement::new(AcceptedFieldKind::Unit, false),
2704                )
2705            })
2706            .collect::<Vec<_>>();
2707        let composite_catalog = AcceptedCompositeCatalog::from_initial_definitions(
2708            BTreeMap::from([(
2709                composite_type_id,
2710                (
2711                    "tests::CompactReplyRecord".to_string(),
2712                    AcceptedCompositeShape::Record(composite_fields),
2713                ),
2714            )]),
2715            &enum_catalog,
2716        )
2717        .expect("bounded reusable record composite should admit");
2718        let encoded = encode_accepted_composite_catalog(&composite_catalog, &enum_catalog)
2719            .expect("reachable composite authority should fit its persisted payload limit");
2720        assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
2721        let composite_catalog = decode_accepted_composite_catalog(&encoded, &enum_catalog)
2722            .expect("persisted composite authority should decode");
2723        AcceptedValueCatalogHandle::new_for_tests(
2724            enum_catalog,
2725            composite_catalog,
2726            AcceptedSchemaRevision::INITIAL,
2727        )
2728    }
2729}