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            PersistedRelationPathStepSnapshot, PersistedRelationSourceSnapshot,
20            PersistedSchemaSnapshot, SchemaHistoricalFill,
21            composite_catalog::{AcceptedCompositeElement, AcceptedCompositeShape},
22            identity_kind_maximum, output_value_from_runtime, query_field_is_queryable,
23            render_accepted_check_expr_sql,
24            runtime::AcceptedRowLayoutRuntimeField,
25        },
26    },
27    error::InternalError,
28    value::{OutputValue, render_output_value_text},
29};
30use std::fmt::Write;
31
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(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
43pub enum SqlColumnKey {
44    /// Accepted primary-key field.
45    Primary,
46    /// Sole field path in one unconditional accepted unique secondary index.
47    Unique,
48    /// Member of a compound, filtered 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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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 source multiplicity: List includes list/map-value traversal; Set denotes set-only traversal."
1002)]
1003#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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        // A predicate limits uniqueness to its matching rows; it cannot supply
1638        // a whole-column UNI hint, even for a single-component key.
1639        Some((index.unique() && index.predicate().is_none(), key_items))
1640    });
1641    classify_compact_column_key(primary, memberships)
1642}
1643
1644fn classify_compact_column_key(
1645    primary: bool,
1646    memberships: impl IntoIterator<Item = (bool, usize)>,
1647) -> SqlColumnKey {
1648    if primary {
1649        return SqlColumnKey::Primary;
1650    }
1651    let mut multiple = false;
1652    for (unique, key_items) in memberships {
1653        if unique && key_items == 1 {
1654            return SqlColumnKey::Unique;
1655        }
1656        multiple = true;
1657    }
1658    if multiple {
1659        SqlColumnKey::Multiple
1660    } else {
1661        SqlColumnKey::None
1662    }
1663}
1664
1665#[cfg_attr(
1666    doc,
1667    doc = "Build field descriptors using accepted persisted schema slot metadata."
1668)]
1669#[cfg(any(test, feature = "sql"))]
1670pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1671    schema: &AcceptedSchemaSnapshot,
1672    value_catalog: &AcceptedValueCatalogHandle,
1673) -> Result<Vec<EntityFieldDescription>, InternalError> {
1674    let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1675    describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1676}
1677
1678fn describe_entity_fields_with_runtime_contract(
1679    schema: &AcceptedSchemaSnapshot,
1680    row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1681    value_catalog: &AcceptedValueCatalogHandle,
1682) -> Result<Vec<EntityFieldDescription>, InternalError> {
1683    let snapshot = schema.persisted_snapshot();
1684    if snapshot.fields().len() != row_layout.fields().len() {
1685        return Err(InternalError::store_invariant());
1686    }
1687    let mut fields = Vec::with_capacity(snapshot.fields().len());
1688
1689    // Accepted-schema describe surfaces must follow the stored schema payload,
1690    // not the generated model's current field order.
1691    for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1692        if field.id() != runtime_field.field_id() {
1693            return Err(InternalError::store_invariant());
1694        }
1695        let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1696        let slot = Some(runtime_field.slot().get());
1697        let metadata = DescribeFieldMetadata::new(
1698            summarize_persisted_field_kind(field.kind(), value_catalog)?,
1699            field.nullable(),
1700            query_field_is_queryable(field.kind(), value_catalog.composite_catalog()),
1701            field_origin_label(field.generated()),
1702        );
1703        let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1704
1705        push_described_field_row(
1706            &mut fields,
1707            field.name(),
1708            slot,
1709            primary_key,
1710            None,
1711            metadata,
1712            temporal,
1713        );
1714
1715        if !field.nested_leaves().is_empty() {
1716            describe_persisted_nested_leaves(
1717                &mut fields,
1718                field.nested_leaves(),
1719                field_origin_label(field.generated()),
1720                value_catalog,
1721            )?;
1722        }
1723    }
1724
1725    Ok(fields)
1726}
1727
1728///
1729/// DescribeFieldMetadata
1730///
1731/// Field-description metadata selected before one field row is rendered.
1732///
1733
1734struct DescribeFieldMetadata {
1735    kind: String,
1736    nullable: bool,
1737    queryable: bool,
1738    origin: String,
1739}
1740
1741impl DescribeFieldMetadata {
1742    // Build one metadata bundle from already-rendered field facts.
1743    const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1744        Self {
1745            kind,
1746            nullable,
1747            queryable,
1748            origin,
1749        }
1750    }
1751}
1752
1753// Add one already-resolved field row to the stable describe DTO list. The
1754// caller owns where metadata came from: generated model or accepted schema.
1755fn push_described_field_row(
1756    fields: &mut Vec<EntityFieldDescription>,
1757    name: &str,
1758    slot: Option<u16>,
1759    primary_key: bool,
1760    tree_prefix: Option<&'static str>,
1761    metadata: DescribeFieldMetadata,
1762    temporal: EntityFieldTemporalFacts,
1763) {
1764    // Nested field rows keep a compact tree marker so table-oriented describe
1765    // output scans as a hierarchy without assigning nested leaves row slots.
1766    let display_name = if let Some(prefix) = tree_prefix {
1767        format!("{prefix}{name}")
1768    } else {
1769        name.to_string()
1770    };
1771
1772    fields.push(EntityFieldDescription::new_with_temporal_facts(
1773        display_name,
1774        slot,
1775        primary_key,
1776        metadata,
1777        temporal,
1778    ));
1779}
1780
1781// Render accepted nested leaf descriptors. Nested leaves do not own physical
1782// row slots, so they always appear with the no-slot sentinel in the Wire DTO.
1783fn describe_persisted_nested_leaves(
1784    fields: &mut Vec<EntityFieldDescription>,
1785    nested_leaves: &[PersistedNestedLeafSnapshot],
1786    origin: String,
1787    value_catalog: &AcceptedValueCatalogHandle,
1788) -> Result<(), InternalError> {
1789    for (index, leaf) in nested_leaves.iter().enumerate() {
1790        let prefix = if index + 1 == nested_leaves.len() {
1791            "└─ "
1792        } else {
1793            "├─ "
1794        };
1795        let name = leaf.path().join(".");
1796        let metadata = DescribeFieldMetadata::new(
1797            summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1798            leaf.nullable(),
1799            query_field_is_queryable(leaf.kind(), value_catalog.composite_catalog()),
1800            origin.clone(),
1801        );
1802
1803        push_described_field_row(
1804            fields,
1805            &name,
1806            None,
1807            false,
1808            Some(prefix),
1809            metadata,
1810            EntityFieldTemporalFacts::nested(),
1811        );
1812    }
1813
1814    Ok(())
1815}
1816
1817fn field_origin_label(generated: bool) -> String {
1818    if generated {
1819        "generated".to_string()
1820    } else {
1821        "ddl".to_string()
1822    }
1823}
1824
1825pub(in crate::db) fn describe_entity_relations_with_persisted_schema(
1826    schema: &AcceptedSchemaSnapshot,
1827    resolve_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
1828) -> Result<Vec<EntityRelationDescription>, InternalError> {
1829    let snapshot = schema.persisted_snapshot();
1830    if snapshot.relations().len() > icydb_schema::MAX_FRAGMENT_RELATIONS {
1831        return Err(InternalError::store_invariant());
1832    }
1833    snapshot
1834        .relations()
1835        .iter()
1836        .map(|relation| {
1837            let local_fields = relation
1838                .source()
1839                .root_field_ids()
1840                .iter()
1841                .map(|field_id| accepted_field_name(snapshot, *field_id))
1842                .collect::<Result<Vec<_>, _>>()?;
1843            let (target_entity_name, target_store_path) = resolve_target(relation.target_path())?;
1844
1845            Ok(EntityRelationDescription::new(
1846                render_primary_key_fields(local_fields.as_slice()),
1847                relation.target_path().to_string(),
1848                target_entity_name,
1849                target_store_path,
1850                persisted_relation_cardinality(snapshot, relation)?,
1851            ))
1852        })
1853        .collect()
1854}
1855
1856fn persisted_relation_cardinality(
1857    snapshot: &PersistedSchemaSnapshot,
1858    relation: &PersistedRelationEdgeSnapshot,
1859) -> Result<EntityRelationCardinality, InternalError> {
1860    let field_ids = match relation.source() {
1861        PersistedRelationSourceSnapshot::Direct { field_ids } => field_ids,
1862        PersistedRelationSourceSnapshot::Nested { steps, .. } => {
1863            // Flattened list/map values can repeat targets even inside a set.
1864            // Retain Set only when every collection step is a set traversal.
1865            let mut cardinality = EntityRelationCardinality::Single;
1866            for step in steps {
1867                match step {
1868                    PersistedRelationPathStepSnapshot::ListItems
1869                    | PersistedRelationPathStepSnapshot::MapValues => {
1870                        return Ok(EntityRelationCardinality::List);
1871                    }
1872                    PersistedRelationPathStepSnapshot::SetItems => {
1873                        cardinality = EntityRelationCardinality::Set;
1874                    }
1875                    _ => {}
1876                }
1877            }
1878            return Ok(cardinality);
1879        }
1880    };
1881    let [field_id] = field_ids.as_slice() else {
1882        return Ok(EntityRelationCardinality::Single);
1883    };
1884    let field = snapshot
1885        .fields()
1886        .iter()
1887        .find(|field| field.id() == *field_id)
1888        .ok_or_else(InternalError::store_invariant)?;
1889
1890    Ok(match field.kind() {
1891        AcceptedFieldKind::List(_) => EntityRelationCardinality::List,
1892        AcceptedFieldKind::Set(_) => EntityRelationCardinality::Set,
1893        _ => EntityRelationCardinality::Single,
1894    })
1895}
1896
1897fn write_accepted_composite_shape_summary(
1898    out: &mut String,
1899    shape: &AcceptedCompositeShape,
1900    value_catalog: &AcceptedValueCatalogHandle,
1901) -> Result<(), InternalError> {
1902    match shape {
1903        AcceptedCompositeShape::Record(fields) => {
1904            out.push_str("record{");
1905            for (index, field) in fields.iter().enumerate() {
1906                if index > 0 {
1907                    out.push_str(", ");
1908                }
1909                out.push_str(field.name());
1910                out.push(':');
1911                write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1912            }
1913            out.push('}');
1914        }
1915        AcceptedCompositeShape::Tuple(elements) => {
1916            out.push_str("tuple<");
1917            for (index, element) in elements.iter().enumerate() {
1918                if index > 0 {
1919                    out.push_str(", ");
1920                }
1921                write_accepted_composite_element_summary(out, element, value_catalog)?;
1922            }
1923            out.push('>');
1924        }
1925        AcceptedCompositeShape::Newtype(inner) => {
1926            out.push_str("newtype<");
1927            write_accepted_composite_element_summary(out, inner, value_catalog)?;
1928            out.push('>');
1929        }
1930    }
1931
1932    Ok(())
1933}
1934
1935fn write_accepted_composite_element_summary(
1936    out: &mut String,
1937    element: &AcceptedCompositeElement,
1938    value_catalog: &AcceptedValueCatalogHandle,
1939) -> Result<(), InternalError> {
1940    write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1941    write_composite_nullability_summary(out, element.nullable());
1942    Ok(())
1943}
1944
1945fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1946    match codec {
1947        CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1948    }
1949}
1950
1951fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1952    if nullable {
1953        out.push('?');
1954    }
1955}
1956
1957// Write the common text/blob describe label. Both generated and accepted schema
1958// summaries use this path so bounded and explicitly unbounded contracts stay
1959// visibly identical across `DESCRIBE` and `SHOW COLUMNS`.
1960fn write_length_bounded_field_kind_summary(
1961    out: &mut String,
1962    kind_name: &str,
1963    max_len: Option<u32>,
1964) {
1965    out.push_str(kind_name);
1966    if let Some(max_len) = max_len {
1967        out.push_str("(max_len=");
1968        out.push_str(&max_len.to_string());
1969        out.push(')');
1970    } else {
1971        out.push_str("(unbounded)");
1972    }
1973}
1974
1975///
1976/// RenderedTemporalPayload
1977///
1978/// One accepted temporal payload projected as an inseparable bounded value,
1979/// byte count, and stable diagnostic hash.
1980///
1981
1982struct RenderedTemporalPayload {
1983    value: String,
1984    bytes: u32,
1985    hash: String,
1986}
1987
1988fn accepted_field_temporal_facts(
1989    field: &AcceptedRowLayoutRuntimeField<'_>,
1990    value_catalog: &AcceptedValueCatalogHandle,
1991) -> Result<EntityFieldTemporalFacts, InternalError> {
1992    let write_policy = field.write_policy();
1993    let insert_omission = if write_policy.insert_generation().is_some() {
1994        "generated"
1995    } else if write_policy.write_management().is_some() {
1996        "managed"
1997    } else {
1998        match field.insert_omission_policy() {
1999            AcceptedInsertOmissionPolicy::NullIfMissing => "null",
2000            AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
2001            AcceptedInsertOmissionPolicy::Required => "required",
2002        }
2003    };
2004    let insert_default = field
2005        .insert_default()
2006        .slot_payload()
2007        .map(|payload| accepted_payload_facts(field, value_catalog, payload))
2008        .transpose()?;
2009    let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
2010        Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
2011        None => (None, None, None),
2012    };
2013    let (historical_fill, historical_fill_bytes, historical_fill_hash) =
2014        match field.historical_fill() {
2015            SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
2016            SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
2017            SchemaHistoricalFill::SlotPayload(payload) => {
2018                let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
2019                (
2020                    Some(rendered.value),
2021                    Some(rendered.bytes),
2022                    Some(rendered.hash),
2023                )
2024            }
2025        };
2026
2027    Ok(EntityFieldTemporalFacts {
2028        insert_omission: Some(insert_omission.to_string()),
2029        insert_default,
2030        insert_default_bytes,
2031        insert_default_hash,
2032        introduced_in_layout: Some(field.introduced_in_layout().get()),
2033        historical_fill,
2034        historical_fill_bytes,
2035        historical_fill_hash,
2036    })
2037}
2038
2039fn accepted_payload_facts(
2040    field: &AcceptedRowLayoutRuntimeField<'_>,
2041    value_catalog: &AcceptedValueCatalogHandle,
2042    payload: &[u8],
2043) -> Result<RenderedTemporalPayload, InternalError> {
2044    let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
2045        .map_err(|_| InternalError::store_invariant())?;
2046    let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
2047    let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.into_value())
2048        .map_err(|_| InternalError::store_invariant())?;
2049    let hash = short_default_payload_fingerprint(payload);
2050    let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
2051    let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
2052
2053    Ok(RenderedTemporalPayload {
2054        value: rendered,
2055        bytes,
2056        hash,
2057    })
2058}
2059
2060fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
2061    let rendered = match value.as_public() {
2062        crate::value::PublicValue::Text(value) => format!("'{}'", value.escape_default()),
2063        _ => render_output_value_text(value),
2064    };
2065    if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
2066        return rendered;
2067    }
2068
2069    format!(
2070        "{}(bytes={}, sha256={})",
2071        output_value_kind_label(value),
2072        payload.len(),
2073        hash,
2074    )
2075}
2076
2077const fn output_value_kind_label(value: &OutputValue) -> &'static str {
2078    match value.as_public() {
2079        crate::value::PublicValue::Account(_) => "account",
2080        crate::value::PublicValue::Blob(_) => "blob",
2081        crate::value::PublicValue::Bool(_) => "bool",
2082        crate::value::PublicValue::Date(_) => "date",
2083        crate::value::PublicValue::Decimal(_) => "decimal",
2084        crate::value::PublicValue::Duration(_) => "duration",
2085        crate::value::PublicValue::Enum(_) => "enum",
2086        crate::value::PublicValue::Float32(_) => "float32",
2087        crate::value::PublicValue::Float64(_) => "float64",
2088        crate::value::PublicValue::Int64(_) => "int64",
2089        crate::value::PublicValue::Int128(_) => "int128",
2090        crate::value::PublicValue::List(_) => "list",
2091        crate::value::PublicValue::Map(_) => "map",
2092        crate::value::PublicValue::Null => "null",
2093        crate::value::PublicValue::Principal(_) => "principal",
2094        crate::value::PublicValue::Subaccount(_) => "subaccount",
2095        crate::value::PublicValue::Text(_) => "text",
2096        crate::value::PublicValue::Timestamp(_) => "timestamp",
2097        crate::value::PublicValue::Nat64(_) => "nat64",
2098        crate::value::PublicValue::Nat128(_) => "nat128",
2099        crate::value::PublicValue::Ulid(_) => "ulid",
2100        crate::value::PublicValue::Unit => "unit",
2101        crate::value::PublicValue::U256(_) => "u256",
2102    }
2103}
2104
2105fn short_default_payload_fingerprint(payload: &[u8]) -> String {
2106    let digest = Sha256::digest(payload);
2107    let mut out = String::with_capacity(16);
2108    for byte in &digest[..8] {
2109        let _ = write!(out, "{byte:02x}");
2110    }
2111    out
2112}
2113
2114#[cfg_attr(
2115    doc,
2116    doc = "Render one stable field-kind label from accepted persisted schema metadata."
2117)]
2118fn summarize_persisted_field_kind(
2119    kind: &AcceptedFieldKind,
2120    value_catalog: &AcceptedValueCatalogHandle,
2121) -> Result<String, InternalError> {
2122    let mut out = String::new();
2123    write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
2124
2125    Ok(out)
2126}
2127
2128// Stream the accepted persisted field-kind label in the stable public
2129// `DESCRIBE` format directly from live schema metadata.
2130fn write_persisted_field_kind_summary(
2131    out: &mut String,
2132    kind: &AcceptedFieldKind,
2133    value_catalog: &AcceptedValueCatalogHandle,
2134) -> Result<(), InternalError> {
2135    if let Some(name) = describe_kind_name(kind) {
2136        out.push_str(name);
2137        return Ok(());
2138    }
2139
2140    match kind {
2141        AcceptedFieldKind::Blob { max_len } => {
2142            write_length_bounded_field_kind_summary(out, "blob", *max_len);
2143        }
2144        AcceptedFieldKind::Decimal { scale } => {
2145            let _ = write!(out, "decimal(scale={scale})");
2146        }
2147        AcceptedFieldKind::Enum { type_id } => {
2148            let definition = value_catalog
2149                .enum_catalog()
2150                .enum_type(*type_id)
2151                .ok_or_else(InternalError::store_invariant)?;
2152            out.push_str("enum(");
2153            out.push_str(definition.path());
2154            out.push(')');
2155        }
2156        AcceptedFieldKind::Text { max_len } => {
2157            write_length_bounded_field_kind_summary(out, "text", *max_len);
2158        }
2159        AcceptedFieldKind::Relation {
2160            target_entity_name,
2161            key_kind,
2162            ..
2163        } => {
2164            out.push_str("relation(target=");
2165            out.push_str(target_entity_name);
2166            out.push_str(", key=");
2167            write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
2168            out.push(')');
2169        }
2170        AcceptedFieldKind::List(inner) => {
2171            out.push_str("list<");
2172            write_persisted_field_kind_summary(out, inner, value_catalog)?;
2173            out.push('>');
2174        }
2175        AcceptedFieldKind::Set(inner) => {
2176            out.push_str("set<");
2177            write_persisted_field_kind_summary(out, inner, value_catalog)?;
2178            out.push('>');
2179        }
2180        AcceptedFieldKind::Map { key, value } => {
2181            out.push_str("map<");
2182            write_persisted_field_kind_summary(out, key, value_catalog)?;
2183            out.push_str(", ");
2184            write_persisted_field_kind_summary(out, value, value_catalog)?;
2185            out.push('>');
2186        }
2187        AcceptedFieldKind::Composite { type_id } => {
2188            let composite_catalog = value_catalog.composite_catalog();
2189            let definition = composite_catalog
2190                .composite_type(*type_id)
2191                .ok_or_else(InternalError::store_invariant)?;
2192            out.push_str("composite(path=");
2193            out.push_str(definition.path());
2194            out.push_str(", codec=");
2195            write_composite_codec_summary(out, definition.codec());
2196            out.push_str(", shape=");
2197            write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
2198            out.push(')');
2199        }
2200        AcceptedFieldKind::Account
2201        | AcceptedFieldKind::Bool
2202        | AcceptedFieldKind::Date
2203        | AcceptedFieldKind::Duration
2204        | AcceptedFieldKind::Float32
2205        | AcceptedFieldKind::Float64
2206        | AcceptedFieldKind::Int8
2207        | AcceptedFieldKind::Int16
2208        | AcceptedFieldKind::Int32
2209        | AcceptedFieldKind::Int64
2210        | AcceptedFieldKind::Int128
2211        | AcceptedFieldKind::Principal
2212        | AcceptedFieldKind::Subaccount
2213        | AcceptedFieldKind::Timestamp
2214        | AcceptedFieldKind::Nat8
2215        | AcceptedFieldKind::Nat16
2216        | AcceptedFieldKind::Nat32
2217        | AcceptedFieldKind::Nat64
2218        | AcceptedFieldKind::Nat128
2219        | AcceptedFieldKind::Ulid
2220        | AcceptedFieldKind::Unit
2221        | AcceptedFieldKind::U256 => return Err(InternalError::store_invariant()),
2222    }
2223
2224    Ok(())
2225}
2226
2227const fn describe_kind_name(kind: &AcceptedFieldKind) -> Option<&'static str> {
2228    Some(match kind {
2229        AcceptedFieldKind::Account => "account",
2230        AcceptedFieldKind::Bool => "bool",
2231        AcceptedFieldKind::Date => "date",
2232        AcceptedFieldKind::Duration => "duration",
2233        AcceptedFieldKind::Float32 => "float32",
2234        AcceptedFieldKind::Float64 => "float64",
2235        AcceptedFieldKind::Int8 => "int8",
2236        AcceptedFieldKind::Int16 => "int16",
2237        AcceptedFieldKind::Int32 => "int32",
2238        AcceptedFieldKind::Int64 => "int64",
2239        AcceptedFieldKind::Int128 => "int128",
2240        AcceptedFieldKind::Principal => "principal",
2241        AcceptedFieldKind::Subaccount => "subaccount",
2242        AcceptedFieldKind::Timestamp => "timestamp",
2243        AcceptedFieldKind::Nat8 => "nat8",
2244        AcceptedFieldKind::Nat16 => "nat16",
2245        AcceptedFieldKind::Nat32 => "nat32",
2246        AcceptedFieldKind::Nat64 => "nat64",
2247        AcceptedFieldKind::Nat128 => "nat128",
2248        AcceptedFieldKind::Ulid => "ulid",
2249        AcceptedFieldKind::Unit => "unit",
2250        AcceptedFieldKind::U256 => "u256",
2251        AcceptedFieldKind::Blob { .. }
2252        | AcceptedFieldKind::Decimal { .. }
2253        | AcceptedFieldKind::Enum { .. }
2254        | AcceptedFieldKind::Text { .. }
2255        | AcceptedFieldKind::Relation { .. }
2256        | AcceptedFieldKind::List(_)
2257        | AcceptedFieldKind::Set(_)
2258        | AcceptedFieldKind::Map { .. }
2259        | AcceptedFieldKind::Composite { .. } => return None,
2260    })
2261}
2262
2263//
2264// TESTS
2265//
2266
2267#[cfg(test)]
2268mod tests {
2269    use std::collections::BTreeMap;
2270
2271    use super::{
2272        EntityFieldDescription, EntityIdentityDescription, EntityRelationCardinality,
2273        EntityRelationDescription, MAX_SCHEMA_VALUE_RENDER_CHARS, SqlColumnDefault, SqlColumnExtra,
2274        SqlColumnKey, SqlColumnSummary, SqlDescribeOutput, SqlShowRelationsOutput,
2275        classify_compact_column_key, compact_column_capacity_from_counts, compact_column_extras,
2276        describe_accepted_constraint, describe_compact_columns_with_persisted_schema,
2277        describe_constraint_activation, describe_entity_constraints_with_persisted_schema,
2278        describe_entity_fields_with_persisted_schema,
2279        describe_entity_relations_with_persisted_schema, nested_path_nullable,
2280        persisted_relation_cardinality,
2281    };
2282    use crate::db::schema::{
2283        AcceptedCheckExprV1, AcceptedCheckValueExprV1, AcceptedCompositeCatalog,
2284        AcceptedConstraintCatalog, AcceptedConstraintKind, AcceptedConstraintSnapshot,
2285        AcceptedFieldKind, AcceptedIndexPredicate, AcceptedNamedTypeIdentity,
2286        AcceptedRuleOperation, AcceptedRuleTarget, AcceptedSchemaFingerprint,
2287        AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
2288        CompositeFieldId, CompositeTypeId, ConstraintActivationKind, ConstraintActivationSnapshot,
2289        ConstraintActivationState, ConstraintId, ConstraintOrigin, ConstraintValidationJob,
2290        FieldId, FieldStorageDecode, LeafCodec, MAX_SCHEMA_SNAPSHOT_BYTES, PersistedFieldSnapshot,
2291        PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2292        PersistedNestedLeafSnapshot, PersistedRelationEdgeSnapshot,
2293        PersistedRelationPathStepSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
2294        SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaVersion,
2295        composite_catalog::{
2296            AcceptedCompositeElement, AcceptedCompositeField, AcceptedCompositeShape,
2297            decode_accepted_composite_catalog, encode_accepted_composite_catalog,
2298        },
2299        decode_persisted_schema_snapshot, empty_accepted_enum_catalog_for_tests,
2300        encode_persisted_schema_snapshot,
2301    };
2302
2303    const REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS: usize = icydb_schema::MAX_FRAGMENT_FIELDS;
2304    const REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES: usize = 95;
2305    const IC_QUERY_REPLY_BYTES: usize = 3 * 1024 * 1024;
2306
2307    fn constraint_description_fixture() -> (PersistedSchemaSnapshot, AcceptedValueCatalogHandle) {
2308        let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2309            SchemaVersion::initial(),
2310            "tests::Account".to_string(),
2311            "Account".to_string(),
2312            FieldId::new(1),
2313            SchemaRowLayout::initial(vec![
2314                (FieldId::new(1), SchemaFieldSlot::new(0)),
2315                (FieldId::new(2), SchemaFieldSlot::new(1)),
2316            ]),
2317            vec![
2318                PersistedFieldSnapshot::new_initial(
2319                    FieldId::new(1),
2320                    "id".to_string(),
2321                    SchemaFieldSlot::new(0),
2322                    AcceptedFieldKind::Ulid,
2323                    Vec::new(),
2324                    false,
2325                    SchemaInsertDefault::None,
2326                    FieldStorageDecode::ByKind,
2327                    LeafCodec::Scalar(ScalarCodec::Ulid),
2328                ),
2329                PersistedFieldSnapshot::new_initial(
2330                    FieldId::new(2),
2331                    "email".to_string(),
2332                    SchemaFieldSlot::new(1),
2333                    AcceptedFieldKind::Text { max_len: None },
2334                    Vec::new(),
2335                    true,
2336                    SchemaInsertDefault::None,
2337                    FieldStorageDecode::ByKind,
2338                    LeafCodec::Scalar(ScalarCodec::Text),
2339                ),
2340            ],
2341            vec![PersistedIndexSnapshot::new(
2342                SchemaIndexId::new(1).expect("test index identity should be non-zero"),
2343                1,
2344                "account_email".to_string(),
2345                "tests::Account::account_email".to_string(),
2346                true,
2347                PersistedIndexKeySnapshot::from_field_paths(vec![
2348                    PersistedIndexFieldPathSnapshot::new(
2349                        FieldId::new(2),
2350                        SchemaFieldSlot::new(1),
2351                        vec!["email".to_string()],
2352                        AcceptedFieldKind::Text { max_len: None },
2353                        true,
2354                    ),
2355                ]),
2356                Some(crate::db::schema::AcceptedIndexPredicate::test_non_null(2)),
2357            )],
2358        );
2359        let catalog = AcceptedConstraintCatalog::initial(
2360            snapshot.fields(),
2361            snapshot.indexes(),
2362            snapshot.relations(),
2363        )
2364        .expect("fixture constraints should build");
2365        let snapshot = snapshot.with_constraint_catalog(catalog);
2366        let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
2367            empty_accepted_enum_catalog_for_tests(),
2368            AcceptedCompositeCatalog::empty(),
2369            AcceptedSchemaRevision::INITIAL,
2370        );
2371        (snapshot, value_catalog)
2372    }
2373
2374    #[test]
2375    fn filtered_unique_constraint_description_exposes_partial_backing_contract() {
2376        let (snapshot, value_catalog) = constraint_description_fixture();
2377        let constraint = snapshot
2378            .constraints()
2379            .iter()
2380            .find(|constraint| constraint.name() == "account_email")
2381            .expect("unique constraint should exist");
2382
2383        let description = describe_accepted_constraint(&snapshot, &value_catalog, constraint)
2384            .expect("accepted unique constraint should describe");
2385        assert_eq!(description.index_id(), Some(1));
2386        assert_eq!(description.index(), Some("account_email"));
2387        assert_eq!(description.predicate_sql(), Some("email IS NOT NULL"));
2388        assert_eq!(description.semantics(), "partial_unique_index_v1");
2389    }
2390
2391    #[test]
2392    fn constraint_descriptions_preserve_live_progress_and_reject_unbound_jobs() {
2393        let (snapshot, values) = constraint_description_fixture();
2394        let catalog = snapshot
2395            .constraint_catalog()
2396            .clone()
2397            .with_added_check_activation(
2398                "pending_check".into(),
2399                ConstraintOrigin::SqlDdl,
2400                AcceptedCheckExprV1::True,
2401                AcceptedSchemaFingerprint::new([1; 32]),
2402                7,
2403            )
2404            .unwrap();
2405        let id = catalog.activations()[0].id();
2406        let catalog = catalog.with_validation_started(id).unwrap();
2407        let job = ConstraintValidationJob::start(
2408            crate::types::EntityTag::new(1),
2409            snapshot.entity_path().into(),
2410            &catalog.activations()[0],
2411            None,
2412        )
2413        .unwrap();
2414        let schema = AcceptedSchemaSnapshot::new(snapshot.clone().with_constraint_catalog(catalog));
2415        let descriptions = describe_entity_constraints_with_persisted_schema(
2416            &schema,
2417            &values,
2418            std::slice::from_ref(&job),
2419        )
2420        .unwrap();
2421        assert!(
2422            descriptions
2423                .windows(2)
2424                .all(|pair| pair[0].id() < pair[1].id())
2425        );
2426        let active = descriptions
2427            .iter()
2428            .find(|row| row.id() == id.get())
2429            .unwrap();
2430        assert_eq!(active.validation_state(), "validating");
2431        assert_eq!(active.validation_progress().unwrap().phase(), "forward");
2432        assert_eq!(active.validation_progress().unwrap().rows_scanned(), 0);
2433
2434        let missing_job =
2435            describe_entity_constraints_with_persisted_schema(&schema, &values, &[]).unwrap_err();
2436        assert_eq!(
2437            missing_job.diagnostic_code(),
2438            icydb_diagnostic_code::DiagnosticCode::StoreInvariantViolation,
2439        );
2440        let unbound_job = describe_entity_constraints_with_persisted_schema(
2441            &AcceptedSchemaSnapshot::new(snapshot),
2442            &values,
2443            &[job],
2444        )
2445        .unwrap_err();
2446        assert_eq!(
2447            unbound_job.diagnostic_code(),
2448            icydb_diagnostic_code::DiagnosticCode::StoreInvariantViolation,
2449        );
2450    }
2451
2452    #[test]
2453    fn row_local_constraint_descriptions_preserve_meaning_across_activation() {
2454        let (snapshot, values) = constraint_description_fixture();
2455        let field_id = FieldId::new(2);
2456        let expression = Box::new(AcceptedCheckExprV1::IsNotNull(
2457            AcceptedCheckValueExprV1::Field(field_id),
2458        ));
2459        let target = AcceptedRuleTarget::new(
2460            field_id,
2461            AcceptedNamedTypeIdentity::Composite(CompositeTypeId::new(1).unwrap()),
2462        );
2463        let operation = Box::new(AcceptedRuleOperation::LengthRangeInclusive { min: 1, max: 10 });
2464        for (accepted, activating, semantics) in [
2465            (
2466                AcceptedConstraintKind::NotNull { field_id },
2467                ConstraintActivationKind::NotNull { field_id },
2468                "not_null_v1",
2469            ),
2470            (
2471                AcceptedConstraintKind::Check {
2472                    expression: expression.clone(),
2473                },
2474                ConstraintActivationKind::Check { expression },
2475                "check_expr_v1",
2476            ),
2477            (
2478                AcceptedConstraintKind::TargetedRule {
2479                    target,
2480                    operation: operation.clone(),
2481                },
2482                ConstraintActivationKind::TargetedRule { target, operation },
2483                "targeted_length_range_v1",
2484            ),
2485        ] {
2486            assert_constraint_description_lifecycle(
2487                &snapshot, &values, accepted, activating, semantics,
2488            );
2489        }
2490    }
2491
2492    fn assert_constraint_description_lifecycle(
2493        snapshot: &PersistedSchemaSnapshot,
2494        values: &AcceptedValueCatalogHandle,
2495        accepted: AcceptedConstraintKind,
2496        activating: ConstraintActivationKind,
2497        semantics: &str,
2498    ) {
2499        let id = ConstraintId::new(10).unwrap();
2500        let constraint = AcceptedConstraintSnapshot::new(
2501            id,
2502            "rule".into(),
2503            ConstraintOrigin::Generated,
2504            accepted,
2505        );
2506        let mut expected = describe_accepted_constraint(snapshot, values, &constraint).unwrap();
2507        assert_eq!(expected.semantics(), semantics);
2508        assert_eq!(expected.fields(), &["email"]);
2509        assert_eq!(expected.validation_state(), "validated");
2510        for state in [
2511            ConstraintActivationState::EnforcingNewWrites,
2512            ConstraintActivationState::Validating,
2513        ] {
2514            let activation = ConstraintActivationSnapshot::new(
2515                id,
2516                "rule".into(),
2517                ConstraintOrigin::Generated,
2518                activating.clone(),
2519                state,
2520                AcceptedSchemaFingerprint::new([1; 32]),
2521                1,
2522            );
2523            let job = (state == ConstraintActivationState::Validating).then(|| {
2524                ConstraintValidationJob::start(
2525                    crate::types::EntityTag::new(1),
2526                    snapshot.entity_path().into(),
2527                    &activation,
2528                    None,
2529                )
2530                .unwrap()
2531            });
2532            if job.is_some() {
2533                assert!(
2534                    describe_constraint_activation(snapshot, values, &activation, None).is_err()
2535                );
2536            }
2537            let description =
2538                describe_constraint_activation(snapshot, values, &activation, job.as_ref())
2539                    .unwrap();
2540            assert_eq!(
2541                description.validation_state(),
2542                if job.is_some() {
2543                    "validating"
2544                } else {
2545                    "enforcing_new_writes"
2546                }
2547            );
2548            assert_eq!(description.validation_progress().is_some(), job.is_some());
2549            expected
2550                .validation_state
2551                .clone_from(&description.validation_state);
2552            expected
2553                .validation_progress
2554                .clone_from(&description.validation_progress);
2555            assert_eq!(description, expected);
2556        }
2557    }
2558
2559    #[test]
2560    fn relation_descriptions_use_the_owner_for_their_lifecycle_state() {
2561        let (snapshot, values) = constraint_description_fixture();
2562        let relation_id = RelationId::new(1).unwrap();
2563        let accepted = PersistedRelationEdgeSnapshot::new_direct(
2564            relation_id,
2565            "accepted_relation".into(),
2566            "AcceptedTarget".into(),
2567            vec![FieldId::new(1)],
2568        );
2569        let candidate = PersistedRelationEdgeSnapshot::new_direct(
2570            relation_id,
2571            "candidate_relation".into(),
2572            "CandidateTarget".into(),
2573            vec![FieldId::new(2)],
2574        );
2575        let snapshot = snapshot
2576            .with_relations(vec![accepted])
2577            .with_constraint_candidates(Vec::new(), vec![candidate]);
2578        let id = ConstraintId::new(10).unwrap();
2579        let constraint = AcceptedConstraintSnapshot::new(
2580            id,
2581            "relation".into(),
2582            ConstraintOrigin::SqlDdl,
2583            AcceptedConstraintKind::Relation { relation_id },
2584        );
2585        let activation = ConstraintActivationSnapshot::new(
2586            id,
2587            "relation".into(),
2588            ConstraintOrigin::SqlDdl,
2589            ConstraintActivationKind::Relation { relation_id },
2590            ConstraintActivationState::EnforcingNewWrites,
2591            AcceptedSchemaFingerprint::new([1; 32]),
2592            1,
2593        );
2594        let accepted = describe_accepted_constraint(&snapshot, &values, &constraint).unwrap();
2595        let candidate =
2596            describe_constraint_activation(&snapshot, &values, &activation, None).unwrap();
2597        assert_eq!(accepted.relation(), Some("accepted_relation"));
2598        assert_eq!(accepted.target_entity(), Some("AcceptedTarget"));
2599        assert_eq!(accepted.fields(), &["id"]);
2600        assert_eq!(candidate.relation(), Some("candidate_relation"));
2601        assert_eq!(candidate.target_entity(), Some("CandidateTarget"));
2602        assert_eq!(candidate.fields(), &["email"]);
2603        assert_eq!(candidate.action(), Some("restrict"));
2604        assert_eq!(candidate.semantics(), "relation_pk_restrict_v1");
2605    }
2606
2607    #[test]
2608    fn identity_description_reports_exact_remaining_capacity_and_exhaustion() {
2609        let available =
2610            EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 254)
2611                .expect("in-domain Identity description should build");
2612        assert_eq!(available.minimum(), 1);
2613        assert_eq!(available.maximum(), 255);
2614        assert_eq!(available.high_water(), 254);
2615        assert_eq!(available.remaining(), 1);
2616        assert!(!available.exhausted());
2617
2618        let exhausted =
2619            EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 255)
2620                .expect("exact-domain exhaustion should remain describable");
2621        assert_eq!(exhausted.remaining(), 0);
2622        assert!(exhausted.exhausted());
2623
2624        assert!(
2625            EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 256).is_err(),
2626            "state beyond the accepted domain must not be described",
2627        );
2628    }
2629
2630    #[test]
2631    fn compact_key_contract_distinguishes_single_unique_from_compound_membership() {
2632        assert_eq!(
2633            classify_compact_column_key(true, [(true, 1), (false, 2)]),
2634            SqlColumnKey::Primary
2635        );
2636        assert_eq!(
2637            classify_compact_column_key(false, [(true, 2)]),
2638            SqlColumnKey::Multiple,
2639            "compound unique membership must not imply independent uniqueness",
2640        );
2641        assert_eq!(
2642            classify_compact_column_key(false, [(false, 1), (true, 1)]),
2643            SqlColumnKey::Unique,
2644            "single-field unique membership has precedence over non-unique membership",
2645        );
2646        assert_eq!(
2647            classify_compact_column_key(false, std::iter::empty()),
2648            SqlColumnKey::None
2649        );
2650    }
2651
2652    #[test]
2653    fn descriptive_column_keys_require_unconditional_uniqueness() {
2654        let (snapshot, values) = constraint_description_fixture();
2655        // An unconditional unique control needs a non-null source; nullable
2656        // unique admission deliberately requires an explicit omission guard.
2657        let email = PersistedFieldSnapshot::new_initial(
2658            FieldId::new(2),
2659            "email".into(),
2660            SchemaFieldSlot::new(1),
2661            AcceptedFieldKind::Text { max_len: None },
2662            Vec::new(),
2663            false,
2664            SchemaInsertDefault::None,
2665            FieldStorageDecode::ByKind,
2666            LeafCodec::Scalar(ScalarCodec::Text),
2667        );
2668        let snapshot = snapshot
2669            .clone()
2670            .with_fields(vec![snapshot.fields()[0].clone(), email]);
2671        let partial = snapshot.indexes()[0]
2672            .clone_with_top_level_field_nullability(FieldId::new(2), false)
2673            .clone_with_predicate_for_tests(Some(AcceptedIndexPredicate::bind_test_sql(
2674                "email <> 'excluded'",
2675                snapshot.fields(),
2676            )));
2677        let key = partial.key().clone();
2678        let unconditional = |ordinal| {
2679            PersistedIndexSnapshot::new(
2680                SchemaIndexId::new(2).unwrap(),
2681                ordinal,
2682                "account_all_email".into(),
2683                "tests::Account::account_all_email".into(),
2684                true,
2685                key.clone(),
2686                None,
2687            )
2688        };
2689        for (indexes, expected) in [
2690            (vec![partial.clone()], SqlColumnKey::Multiple),
2691            (vec![unconditional(1)], SqlColumnKey::Unique),
2692            (vec![partial, unconditional(2)], SqlColumnKey::Unique),
2693            (Vec::new(), SqlColumnKey::None),
2694        ] {
2695            let snapshot = snapshot.clone().with_indexes(indexes);
2696            let constraints = AcceptedConstraintCatalog::initial(
2697                snapshot.fields(),
2698                snapshot.indexes(),
2699                snapshot.relations(),
2700            )
2701            .unwrap();
2702            let schema = AcceptedSchemaSnapshot::try_new_with_acceptance(
2703                snapshot.with_constraint_catalog(constraints),
2704            )
2705            .unwrap();
2706            let columns = describe_compact_columns_with_persisted_schema(&schema, &values).unwrap();
2707            assert_eq!(columns[0].key(), SqlColumnKey::Primary);
2708            assert_eq!(columns[1].key(), expected);
2709        }
2710    }
2711
2712    // An accepted record with distinct address.city and city leaves lets the
2713    // compact and verbose projections qualify the same metadata authority.
2714    fn nested_description_fixture() -> (AcceptedSchemaSnapshot, AcceptedValueCatalogHandle) {
2715        let (snapshot, _) = constraint_description_fixture();
2716        let profile_id = CompositeTypeId::new(1).unwrap();
2717        let address_id = CompositeTypeId::new(2).unwrap();
2718        let text = AcceptedFieldKind::Text { max_len: None };
2719        let member = |id, name: &str, kind, nullable| {
2720            AcceptedCompositeField::new(
2721                CompositeFieldId::new(id).unwrap(),
2722                name.into(),
2723                AcceptedCompositeElement::new(kind, nullable),
2724            )
2725        };
2726        let enums = empty_accepted_enum_catalog_for_tests();
2727        let profile_shape = AcceptedCompositeShape::Record(vec![
2728            member(
2729                1,
2730                "address",
2731                AcceptedFieldKind::Composite {
2732                    type_id: address_id,
2733                },
2734                false,
2735            ),
2736            member(2, "city", text.clone(), true),
2737        ]);
2738        let address_shape =
2739            AcceptedCompositeShape::Record(vec![member(1, "city", text.clone(), false)]);
2740        let composites = AcceptedCompositeCatalog::from_initial_definitions(
2741            BTreeMap::from([
2742                (profile_id, ("tests::Profile".into(), profile_shape)),
2743                (address_id, ("tests::Address".into(), address_shape)),
2744            ]),
2745            &enums,
2746        )
2747        .unwrap();
2748        let values = AcceptedValueCatalogHandle::new_for_tests(
2749            enums,
2750            composites,
2751            AcceptedSchemaRevision::INITIAL,
2752        );
2753        let leaves = vec![
2754            PersistedNestedLeafSnapshot::new(
2755                vec!["address".into(), "city".into()],
2756                text.clone(),
2757                false,
2758            ),
2759            PersistedNestedLeafSnapshot::new(vec!["city".into()], text, true),
2760        ];
2761        let profile = PersistedFieldSnapshot::new_initial(
2762            FieldId::new(2),
2763            "profile".into(),
2764            SchemaFieldSlot::new(1),
2765            AcceptedFieldKind::Composite {
2766                type_id: profile_id,
2767            },
2768            leaves,
2769            false,
2770            SchemaInsertDefault::None,
2771            FieldStorageDecode::CatalogValue,
2772            LeafCodec::Structural,
2773        );
2774        let snapshot = snapshot
2775            .clone()
2776            .with_fields(vec![snapshot.fields()[0].clone(), profile])
2777            .with_indexes(Vec::new());
2778        let constraints = AcceptedConstraintCatalog::initial(
2779            snapshot.fields(),
2780            snapshot.indexes(),
2781            snapshot.relations(),
2782        )
2783        .unwrap();
2784        let schema =
2785            AcceptedSchemaSnapshot::try_new(snapshot.with_constraint_catalog(constraints)).unwrap();
2786
2787        (schema, values)
2788    }
2789
2790    #[test]
2791    fn descriptive_nested_names_retain_the_complete_relative_path() {
2792        let (schema, values) = nested_description_fixture();
2793        let fields = describe_entity_fields_with_persisted_schema(&schema, &values).unwrap();
2794        let columns = describe_compact_columns_with_persisted_schema(&schema, &values).unwrap();
2795        assert_eq!(fields[1].name(), "profile");
2796        for (index, display, canonical) in [
2797            (2, "├─ address.city", "profile.address.city"),
2798            (3, "└─ city", "profile.city"),
2799        ] {
2800            assert_eq!(fields[index].name(), display);
2801            assert_eq!(columns[index].name(), canonical);
2802            assert_eq!(fields[index].slot(), None);
2803            assert_eq!(fields[index].nullable(), columns[index].nullable());
2804        }
2805        assert!(!fields[2].nullable());
2806        assert!(fields[3].nullable());
2807    }
2808
2809    #[test]
2810    fn descriptive_nested_relations_report_collection_multiplicity() {
2811        use PersistedRelationPathStepSnapshot::{ListItems, MapValues, OptionalSome, SetItems};
2812        // Optional/scalar traversal stays single. Any sequence/map fanout is
2813        // list-like; a path with only set fanout retains the existing Set hint.
2814        for (steps, expected) in [
2815            (vec![OptionalSome], EntityRelationCardinality::Single),
2816            (
2817                vec![OptionalSome, ListItems],
2818                EntityRelationCardinality::List,
2819            ),
2820            (vec![SetItems], EntityRelationCardinality::Set),
2821            (vec![MapValues], EntityRelationCardinality::List),
2822            (vec![SetItems, ListItems], EntityRelationCardinality::List),
2823            (vec![ListItems, SetItems], EntityRelationCardinality::List),
2824            (vec![SetItems, SetItems], EntityRelationCardinality::Set),
2825            (vec![SetItems, MapValues], EntityRelationCardinality::List),
2826        ] {
2827            let kind = steps
2828                .iter()
2829                .rev()
2830                .fold(AcceptedFieldKind::Ulid, |kind, step| match step {
2831                    ListItems => AcceptedFieldKind::List(Box::new(kind)),
2832                    SetItems => AcceptedFieldKind::Set(Box::new(kind)),
2833                    MapValues => AcceptedFieldKind::Map {
2834                        key: Box::new(AcceptedFieldKind::Nat64),
2835                        value: Box::new(kind),
2836                    },
2837                    OptionalSome => kind,
2838                    _ => unreachable!("fixture contains only collection/optional traversal"),
2839                });
2840            let nullable = steps.first() == Some(&OptionalSome);
2841            let relation = PersistedRelationEdgeSnapshot::new_nested(
2842                RelationId::new(1).unwrap(),
2843                "nested_owner".into(),
2844                "tests::Owner".into(),
2845                FieldId::new(2),
2846                steps,
2847            );
2848            let schema = relation_description_fixture(kind, nullable, relation);
2849            let rows = describe_entity_relations_with_persisted_schema(&schema, &|_| {
2850                Ok(("Owner".into(), "tests::OwnerStore".into()))
2851            })
2852            .unwrap();
2853            assert_eq!(rows.len(), 1);
2854            assert_eq!(rows[0].cardinality(), expected);
2855        }
2856    }
2857
2858    // Keep relation projections rooted in an admitted source shape rather than
2859    // supplying collection steps over an unrelated scalar field.
2860    fn relation_description_fixture(
2861        kind: AcceptedFieldKind,
2862        nullable: bool,
2863        relation: PersistedRelationEdgeSnapshot,
2864    ) -> AcceptedSchemaSnapshot {
2865        let (snapshot, _) = constraint_description_fixture();
2866        let (storage, codec) = if kind == AcceptedFieldKind::Ulid {
2867            (
2868                FieldStorageDecode::ByKind,
2869                LeafCodec::Scalar(ScalarCodec::Ulid),
2870            )
2871        } else {
2872            (FieldStorageDecode::CatalogValue, LeafCodec::Structural)
2873        };
2874        let source = PersistedFieldSnapshot::new_initial(
2875            FieldId::new(2),
2876            "owner".into(),
2877            SchemaFieldSlot::new(1),
2878            kind,
2879            Vec::new(),
2880            nullable,
2881            SchemaInsertDefault::None,
2882            storage,
2883            codec,
2884        );
2885        let snapshot = snapshot
2886            .clone()
2887            .with_fields(vec![snapshot.fields()[0].clone(), source])
2888            .with_indexes(Vec::new())
2889            .with_relations(vec![relation]);
2890        let constraints = AcceptedConstraintCatalog::initial(
2891            snapshot.fields(),
2892            snapshot.indexes(),
2893            snapshot.relations(),
2894        )
2895        .unwrap();
2896        AcceptedSchemaSnapshot::try_new(snapshot.with_constraint_catalog(constraints)).unwrap()
2897    }
2898
2899    #[test]
2900    fn descriptive_direct_relations_preserve_scalar_tuple_list_and_set_hints() {
2901        for (kind, expected) in [
2902            (AcceptedFieldKind::Ulid, EntityRelationCardinality::Single),
2903            (
2904                AcceptedFieldKind::List(Box::new(AcceptedFieldKind::Ulid)),
2905                EntityRelationCardinality::List,
2906            ),
2907            (
2908                AcceptedFieldKind::Set(Box::new(AcceptedFieldKind::Ulid)),
2909                EntityRelationCardinality::Set,
2910            ),
2911        ] {
2912            let relation = PersistedRelationEdgeSnapshot::new_direct(
2913                RelationId::new(1).unwrap(),
2914                "owner".into(),
2915                "tests::Owner".into(),
2916                vec![FieldId::new(2)],
2917            );
2918            let schema = relation_description_fixture(kind, false, relation);
2919            assert_eq!(
2920                persisted_relation_cardinality(
2921                    schema.persisted_snapshot(),
2922                    &schema.persisted_snapshot().relations()[0]
2923                )
2924                .unwrap(),
2925                expected
2926            );
2927        }
2928        let (snapshot, _) = constraint_description_fixture();
2929        let tuple = PersistedRelationEdgeSnapshot::new_direct(
2930            RelationId::new(1).unwrap(),
2931            "tuple_owner".into(),
2932            "tests::Owner".into(),
2933            vec![FieldId::new(1), FieldId::new(2)],
2934        );
2935        assert_eq!(
2936            persisted_relation_cardinality(&snapshot, &tuple).unwrap(),
2937            EntityRelationCardinality::Single
2938        );
2939    }
2940
2941    #[test]
2942    fn compact_extra_contract_is_closed_and_deterministically_ordered() {
2943        assert_eq!(
2944            compact_column_extras(true, true, true),
2945            vec![
2946                SqlColumnExtra::Identity,
2947                SqlColumnExtra::Generated,
2948                SqlColumnExtra::Relation,
2949            ]
2950        );
2951        assert_eq!(
2952            compact_column_extras(false, true, false),
2953            vec![SqlColumnExtra::Generated]
2954        );
2955        assert!(compact_column_extras(false, false, false).is_empty());
2956    }
2957
2958    #[test]
2959    fn compact_projection_bounds_accept_maximum_and_reject_max_plus_one() {
2960        assert_eq!(
2961            compact_column_capacity_from_counts(
2962                icydb_schema::MAX_FRAGMENT_FIELDS,
2963                std::iter::repeat_n(
2964                    icydb_schema::MAX_FRAGMENT_FIELDS,
2965                    icydb_schema::MAX_FRAGMENT_FIELDS,
2966                ),
2967            )
2968            .expect("accepted maximum should remain projectable"),
2969            super::MAX_SQL_COMPACT_COLUMN_ROWS,
2970        );
2971        assert!(
2972            compact_column_capacity_from_counts(
2973                icydb_schema::MAX_FRAGMENT_FIELDS + 1,
2974                std::iter::repeat_n(0, icydb_schema::MAX_FRAGMENT_FIELDS + 1),
2975            )
2976            .is_err()
2977        );
2978        assert!(
2979            compact_column_capacity_from_counts(1, [icydb_schema::MAX_FRAGMENT_FIELDS + 1],)
2980                .is_err()
2981        );
2982
2983        let valid = SqlColumnSummary::new(
2984            "value".to_string(),
2985            "text".to_string(),
2986            false,
2987            SqlColumnKey::None,
2988            SqlColumnDefault::Literal {
2989                text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS),
2990            },
2991            vec![
2992                SqlColumnExtra::Identity,
2993                SqlColumnExtra::Generated,
2994                SqlColumnExtra::Relation,
2995            ],
2996        );
2997        let valid = valid.expect("the complete admitted compact row should remain valid");
2998        assert!(
2999            SqlColumnSummary::new(
3000                "value".to_string(),
3001                "text".to_string(),
3002                false,
3003                SqlColumnKey::None,
3004                SqlColumnDefault::Literal {
3005                    text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS + 1),
3006                },
3007                Vec::new(),
3008            )
3009            .is_err()
3010        );
3011        assert!(
3012            SqlColumnSummary::new(
3013                "value".to_string(),
3014                "text".to_string(),
3015                false,
3016                SqlColumnKey::None,
3017                SqlColumnDefault::Required,
3018                vec![SqlColumnExtra::Generated; 4],
3019            )
3020            .is_err()
3021        );
3022
3023        let maximum = SqlDescribeOutput::Compact {
3024            entity: "AcceptedMaximum".to_string(),
3025            columns: vec![valid; super::MAX_SQL_COMPACT_COLUMN_ROWS],
3026        };
3027        assert!(matches!(
3028            icydb_schema::wire::encoded_len(&maximum),
3029            Err(icydb_schema::wire::Error::Limit),
3030        ));
3031        assert!(matches!(
3032            icydb_schema::wire::encode_one(&maximum),
3033            Err(icydb_schema::wire::Error::Limit),
3034        ));
3035
3036        let relation = EntityRelationDescription::new(
3037            "owner_id".to_string(),
3038            "entities::Owner".to_string(),
3039            "Owner".to_string(),
3040            "stores::Owner".to_string(),
3041            EntityRelationCardinality::Single,
3042        );
3043        assert!(
3044            SqlShowRelationsOutput::new(
3045                "Entry".to_string(),
3046                vec![relation.clone(); icydb_schema::MAX_FRAGMENT_RELATIONS],
3047            )
3048            .is_ok()
3049        );
3050        assert!(
3051            SqlShowRelationsOutput::new(
3052                "Entry".to_string(),
3053                vec![relation; icydb_schema::MAX_FRAGMENT_RELATIONS + 1],
3054            )
3055            .is_err()
3056        );
3057    }
3058
3059    #[test]
3060    fn reachable_accepted_compact_projection_exceeds_the_public_query_reply_limit() {
3061        let accepted = reachable_compact_reply_schema();
3062        let value_catalog = reachable_compact_reply_value_catalog();
3063        let columns = describe_compact_columns_with_persisted_schema(&accepted, &value_catalog)
3064            .expect("reachable accepted schema should project compact columns");
3065
3066        assert_eq!(
3067            columns.len(),
3068            1 + REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES
3069                * (1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS),
3070        );
3071        let output = SqlDescribeOutput::Compact {
3072            entity: accepted.entity_name().to_string(),
3073            columns,
3074        };
3075        let encoded_len = icydb_schema::wire::encoded_len(&output)
3076            .expect("reachable accepted compact output should admit allocation-free Wire sizing");
3077        assert!(
3078            encoded_len > IC_QUERY_REPLY_BYTES,
3079            "a valid accepted schema must exercise the generated endpoint reply guard",
3080        );
3081        assert!(matches!(
3082            icydb_schema::wire::encode_one(&output),
3083            Err(icydb_schema::wire::Error::Limit),
3084        ));
3085    }
3086
3087    #[test]
3088    fn field_descriptions_preserve_root_and_nested_queryability() {
3089        let accepted = reachable_compact_reply_schema();
3090        let value_catalog = reachable_compact_reply_value_catalog();
3091        let fields = describe_entity_fields_with_persisted_schema(&accepted, &value_catalog)
3092            .expect("accepted root and leaf metadata should describe");
3093
3094        assert!(fields[0].queryable());
3095        let (groups, remainder) =
3096            fields[1..].as_chunks::<{ 1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS }>();
3097        assert!(remainder.is_empty());
3098        assert_eq!(groups.len(), REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES);
3099        for group in groups {
3100            assert!(!group[0].queryable(), "record roots remain non-queryable");
3101            assert!(group[1..].iter().all(EntityFieldDescription::queryable));
3102        }
3103    }
3104
3105    #[test]
3106    fn nested_nullability_includes_nullable_ancestors() {
3107        let leaves = vec![
3108            PersistedNestedLeafSnapshot::new(
3109                vec!["address".to_string()],
3110                AcceptedFieldKind::Unit,
3111                true,
3112            ),
3113            PersistedNestedLeafSnapshot::new(
3114                vec!["address".to_string(), "city".to_string()],
3115                AcceptedFieldKind::Unit,
3116                false,
3117            ),
3118        ];
3119        assert!(nested_path_nullable(
3120            false,
3121            leaves.as_slice(),
3122            &["address".to_string(), "city".to_string()],
3123        ));
3124        assert!(nested_path_nullable(
3125            true,
3126            leaves.as_slice(),
3127            &["other".to_string()],
3128        ));
3129        assert!(!nested_path_nullable(
3130            false,
3131            leaves.as_slice(),
3132            &["other".to_string()],
3133        ));
3134    }
3135
3136    fn compact_reply_leaf_name(index: usize) -> String {
3137        let first = u8::try_from(index / 26).expect("bounded leaf prefix fits u8") + b'a';
3138        let second = u8::try_from(index % 26).expect("bounded leaf suffix fits u8") + b'a';
3139        String::from_utf8(vec![first, second]).expect("ASCII leaf name should be UTF-8")
3140    }
3141
3142    fn compact_reply_top_level_name(index: usize) -> String {
3143        let prefix = format!("field_{index:03}_");
3144        format!("{prefix}{}", "x".repeat(128 - prefix.len()))
3145    }
3146
3147    fn reachable_compact_reply_schema() -> AcceptedSchemaSnapshot {
3148        let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
3149        let nested_leaves = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
3150            .map(|index| {
3151                PersistedNestedLeafSnapshot::new(
3152                    vec![compact_reply_leaf_name(index)],
3153                    AcceptedFieldKind::Unit,
3154                    false,
3155                )
3156            })
3157            .collect::<Vec<_>>();
3158        let mut fields = vec![PersistedFieldSnapshot::new_initial(
3159            FieldId::new(1),
3160            "id".to_string(),
3161            SchemaFieldSlot::new(0),
3162            AcceptedFieldKind::Nat64,
3163            Vec::new(),
3164            false,
3165            SchemaInsertDefault::None,
3166            FieldStorageDecode::ByKind,
3167            LeafCodec::Scalar(ScalarCodec::Nat64),
3168        )];
3169        let mut layout = vec![(FieldId::new(1), SchemaFieldSlot::new(0))];
3170        for index in 0..REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES {
3171            let raw_id = u32::try_from(index)
3172                .expect("bounded top-level index fits u32")
3173                .checked_add(2)
3174                .expect("bounded top-level identity has a successor");
3175            let raw_slot = u16::try_from(index)
3176                .expect("bounded top-level index fits u16")
3177                .checked_add(1)
3178                .expect("bounded top-level slot has a successor");
3179            let id = FieldId::new(raw_id);
3180            let slot = SchemaFieldSlot::new(raw_slot);
3181            fields.push(PersistedFieldSnapshot::new_initial(
3182                id,
3183                compact_reply_top_level_name(index),
3184                slot,
3185                AcceptedFieldKind::Composite {
3186                    type_id: composite_type_id,
3187                },
3188                nested_leaves.clone(),
3189                false,
3190                SchemaInsertDefault::None,
3191                FieldStorageDecode::ByKind,
3192                LeafCodec::Structural,
3193            ));
3194            layout.push((id, slot));
3195        }
3196        let persisted = PersistedSchemaSnapshot::new(
3197            SchemaVersion::initial(),
3198            "tests::ReachableCompactReply".to_string(),
3199            "ReachableCompactReply".to_string(),
3200            FieldId::new(1),
3201            SchemaRowLayout::initial(layout),
3202            fields,
3203        );
3204        let encoded = encode_persisted_schema_snapshot(&persisted)
3205            .expect("reachable compact-reply schema should fit its persisted payload limit");
3206        assert_eq!(encoded.len(), 310_865);
3207        assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
3208        AcceptedSchemaSnapshot::try_new(
3209            decode_persisted_schema_snapshot(encoded.as_slice())
3210                .expect("persisted compact-reply schema should decode"),
3211        )
3212        .expect("decoded compact-reply schema should satisfy accepted integrity")
3213    }
3214
3215    fn reachable_compact_reply_value_catalog() -> AcceptedValueCatalogHandle {
3216        let enum_catalog = empty_accepted_enum_catalog_for_tests();
3217        let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
3218        let composite_fields = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
3219            .map(|index| {
3220                let raw_id = u32::try_from(index)
3221                    .expect("bounded composite index fits u32")
3222                    .checked_add(1)
3223                    .expect("bounded composite identity has a successor");
3224                AcceptedCompositeField::new(
3225                    CompositeFieldId::new(raw_id).expect("composite field identity is non-zero"),
3226                    compact_reply_leaf_name(index),
3227                    AcceptedCompositeElement::new(AcceptedFieldKind::Unit, false),
3228                )
3229            })
3230            .collect::<Vec<_>>();
3231        let composite_catalog = AcceptedCompositeCatalog::from_initial_definitions(
3232            BTreeMap::from([(
3233                composite_type_id,
3234                (
3235                    "tests::CompactReplyRecord".to_string(),
3236                    AcceptedCompositeShape::Record(composite_fields),
3237                ),
3238            )]),
3239            &enum_catalog,
3240        )
3241        .expect("bounded reusable record composite should admit");
3242        let encoded = encode_accepted_composite_catalog(&composite_catalog, &enum_catalog)
3243            .expect("reachable composite authority should fit its persisted payload limit");
3244        assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
3245        let composite_catalog = decode_accepted_composite_catalog(&encoded, &enum_catalog)
3246            .expect("persisted composite authority should decode");
3247        AcceptedValueCatalogHandle::new_for_tests(
3248            enum_catalog,
3249            composite_catalog,
3250            AcceptedSchemaRevision::INITIAL,
3251        )
3252    }
3253}