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