Skip to main content

icydb_core/db/
describe.rs

1//! Module: 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 `EntityModel`/`FieldKind` into stable describe surfaces.
5
6use crate::model::{
7    entity::EntityModel,
8    field::{FieldKind, RelationStrength},
9};
10use candid::CandidType;
11use serde::{Deserialize, Serialize};
12
13///
14/// EntitySchemaDescription
15///
16/// Stable schema-introspection payload for one runtime entity model.
17/// This mirrors SQL-style `DESCRIBE` intent for fields, indexes, and relations.
18///
19
20#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
21pub struct EntitySchemaDescription {
22    pub entity_path: String,
23    pub entity_name: String,
24    pub primary_key: String,
25    pub fields: Vec<EntityFieldDescription>,
26    pub indexes: Vec<EntityIndexDescription>,
27    pub relations: Vec<EntityRelationDescription>,
28}
29
30///
31/// EntityFieldDescription
32///
33/// One field-level projection inside `EntitySchemaDescription`.
34/// Keeps field type and queryability metadata explicit for diagnostics.
35///
36
37#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
38pub struct EntityFieldDescription {
39    pub name: String,
40    pub kind: String,
41    pub primary_key: bool,
42    pub queryable: bool,
43}
44
45///
46/// EntityIndexDescription
47///
48/// One secondary-index projection inside `EntitySchemaDescription`.
49/// Includes uniqueness and ordered field list.
50///
51
52#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
53pub struct EntityIndexDescription {
54    pub name: String,
55    pub unique: bool,
56    pub fields: Vec<String>,
57}
58
59///
60/// EntityRelationDescription
61///
62/// One relation-field projection inside `EntitySchemaDescription`.
63/// Captures relation target identity plus strength/cardinality metadata.
64///
65
66#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub struct EntityRelationDescription {
68    pub field: String,
69    pub target_path: String,
70    pub target_entity_name: String,
71    pub target_store_path: String,
72    pub strength: EntityRelationStrength,
73    pub cardinality: EntityRelationCardinality,
74}
75
76///
77/// EntityRelationStrength
78///
79/// Describe-surface relation strength projection.
80///
81#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
82pub enum EntityRelationStrength {
83    Strong,
84    Weak,
85}
86
87///
88/// EntityRelationCardinality
89///
90/// Describe-surface relation cardinality projection.
91///
92#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
93pub enum EntityRelationCardinality {
94    Single,
95    List,
96    Set,
97}
98
99/// Build one stable entity-schema description from one runtime `EntityModel`.
100#[must_use]
101pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
102    let mut fields = Vec::with_capacity(model.fields.len());
103    let mut relations = Vec::new();
104    for field in model.fields {
105        let field_kind = summarize_field_kind(&field.kind);
106        let queryable = field.kind.value_kind().is_queryable();
107        let primary_key = field.name == model.primary_key.name;
108
109        fields.push(EntityFieldDescription {
110            name: field.name.to_string(),
111            kind: field_kind,
112            primary_key,
113            queryable,
114        });
115
116        if let Some(relation) = relation_from_field_kind(field.name, &field.kind) {
117            relations.push(relation);
118        }
119    }
120
121    let mut indexes = Vec::with_capacity(model.indexes.len());
122    for index in model.indexes {
123        indexes.push(EntityIndexDescription {
124            name: index.name.to_string(),
125            unique: index.unique,
126            fields: index
127                .fields
128                .iter()
129                .map(|field| (*field).to_string())
130                .collect(),
131        });
132    }
133
134    EntitySchemaDescription {
135        entity_path: model.path.to_string(),
136        entity_name: model.entity_name.to_string(),
137        primary_key: model.primary_key.name.to_string(),
138        fields,
139        indexes,
140        relations,
141    }
142}
143
144// Resolve relation metadata from one field kind, including list/set relation forms.
145fn relation_from_field_kind(
146    field_name: &str,
147    kind: &FieldKind,
148) -> Option<EntityRelationDescription> {
149    match kind {
150        FieldKind::Relation {
151            target_path,
152            target_entity_name,
153            target_store_path,
154            strength,
155            ..
156        } => Some(EntityRelationDescription {
157            field: field_name.to_string(),
158            target_path: (*target_path).to_string(),
159            target_entity_name: (*target_entity_name).to_string(),
160            target_store_path: (*target_store_path).to_string(),
161            strength: relation_strength(*strength),
162            cardinality: EntityRelationCardinality::Single,
163        }),
164        FieldKind::List(inner) => {
165            relation_from_collection_relation(field_name, inner, EntityRelationCardinality::List)
166        }
167        FieldKind::Set(inner) => {
168            relation_from_collection_relation(field_name, inner, EntityRelationCardinality::Set)
169        }
170        FieldKind::Account
171        | FieldKind::Blob
172        | FieldKind::Bool
173        | FieldKind::Date
174        | FieldKind::Decimal { .. }
175        | FieldKind::Duration
176        | FieldKind::Enum { .. }
177        | FieldKind::Float32
178        | FieldKind::Float64
179        | FieldKind::Int
180        | FieldKind::Int128
181        | FieldKind::IntBig
182        | FieldKind::Principal
183        | FieldKind::Subaccount
184        | FieldKind::Text
185        | FieldKind::Timestamp
186        | FieldKind::Uint
187        | FieldKind::Uint128
188        | FieldKind::UintBig
189        | FieldKind::Ulid
190        | FieldKind::Unit
191        | FieldKind::Map { .. }
192        | FieldKind::Structured { .. } => None,
193    }
194}
195
196// Resolve list/set relation metadata only when the collection inner shape is relation.
197fn relation_from_collection_relation(
198    field_name: &str,
199    inner: &FieldKind,
200    cardinality: EntityRelationCardinality,
201) -> Option<EntityRelationDescription> {
202    let FieldKind::Relation {
203        target_path,
204        target_entity_name,
205        target_store_path,
206        strength,
207        ..
208    } = inner
209    else {
210        return None;
211    };
212
213    Some(EntityRelationDescription {
214        field: field_name.to_string(),
215        target_path: (*target_path).to_string(),
216        target_entity_name: (*target_entity_name).to_string(),
217        target_store_path: (*target_store_path).to_string(),
218        strength: relation_strength(*strength),
219        cardinality,
220    })
221}
222
223// Project runtime relation strength into the describe DTO surface.
224const fn relation_strength(strength: RelationStrength) -> EntityRelationStrength {
225    match strength {
226        RelationStrength::Strong => EntityRelationStrength::Strong,
227        RelationStrength::Weak => EntityRelationStrength::Weak,
228    }
229}
230
231// Render one stable field-kind label for describe output.
232fn summarize_field_kind(kind: &FieldKind) -> String {
233    match kind {
234        FieldKind::Account => "account".to_string(),
235        FieldKind::Blob => "blob".to_string(),
236        FieldKind::Bool => "bool".to_string(),
237        FieldKind::Date => "date".to_string(),
238        FieldKind::Decimal { scale } => format!("decimal(scale={scale})"),
239        FieldKind::Duration => "duration".to_string(),
240        FieldKind::Enum { path } => format!("enum({path})"),
241        FieldKind::Float32 => "float32".to_string(),
242        FieldKind::Float64 => "float64".to_string(),
243        FieldKind::Int => "int".to_string(),
244        FieldKind::Int128 => "int128".to_string(),
245        FieldKind::IntBig => "int_big".to_string(),
246        FieldKind::Principal => "principal".to_string(),
247        FieldKind::Subaccount => "subaccount".to_string(),
248        FieldKind::Text => "text".to_string(),
249        FieldKind::Timestamp => "timestamp".to_string(),
250        FieldKind::Uint => "uint".to_string(),
251        FieldKind::Uint128 => "uint128".to_string(),
252        FieldKind::UintBig => "uint_big".to_string(),
253        FieldKind::Ulid => "ulid".to_string(),
254        FieldKind::Unit => "unit".to_string(),
255        FieldKind::Relation {
256            target_entity_name,
257            key_kind,
258            strength,
259            ..
260        } => format!(
261            "relation(target={target_entity_name}, key={}, strength={})",
262            summarize_field_kind(key_kind),
263            summarize_relation_strength(*strength),
264        ),
265        FieldKind::List(inner) => format!("list<{}>", summarize_field_kind(inner)),
266        FieldKind::Set(inner) => format!("set<{}>", summarize_field_kind(inner)),
267        FieldKind::Map { key, value } => {
268            format!(
269                "map<{}, {}>",
270                summarize_field_kind(key),
271                summarize_field_kind(value)
272            )
273        }
274        FieldKind::Structured { queryable } => format!("structured(queryable={queryable})"),
275    }
276}
277
278// Render one stable relation-strength label for field-kind summaries.
279const fn summarize_relation_strength(strength: RelationStrength) -> &'static str {
280    match strength {
281        RelationStrength::Strong => "strong",
282        RelationStrength::Weak => "weak",
283    }
284}