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