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