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