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(crate) entity_path: String,
23    pub(crate) entity_name: String,
24    pub(crate) primary_key: String,
25    pub(crate) fields: Vec<EntityFieldDescription>,
26    pub(crate) indexes: Vec<EntityIndexDescription>,
27    pub(crate) relations: Vec<EntityRelationDescription>,
28}
29
30impl EntitySchemaDescription {
31    /// Construct one entity schema description payload.
32    #[must_use]
33    pub const fn new(
34        entity_path: String,
35        entity_name: String,
36        primary_key: String,
37        fields: Vec<EntityFieldDescription>,
38        indexes: Vec<EntityIndexDescription>,
39        relations: Vec<EntityRelationDescription>,
40    ) -> Self {
41        Self {
42            entity_path,
43            entity_name,
44            primary_key,
45            fields,
46            indexes,
47            relations,
48        }
49    }
50
51    /// Borrow the entity module path.
52    #[must_use]
53    pub const fn entity_path(&self) -> &str {
54        self.entity_path.as_str()
55    }
56
57    /// Borrow the entity display name.
58    #[must_use]
59    pub const fn entity_name(&self) -> &str {
60        self.entity_name.as_str()
61    }
62
63    /// Borrow the primary-key field name.
64    #[must_use]
65    pub const fn primary_key(&self) -> &str {
66        self.primary_key.as_str()
67    }
68
69    /// Borrow field description entries.
70    #[must_use]
71    pub const fn fields(&self) -> &[EntityFieldDescription] {
72        self.fields.as_slice()
73    }
74
75    /// Borrow index description entries.
76    #[must_use]
77    pub const fn indexes(&self) -> &[EntityIndexDescription] {
78        self.indexes.as_slice()
79    }
80
81    /// Borrow relation description entries.
82    #[must_use]
83    pub const fn relations(&self) -> &[EntityRelationDescription] {
84        self.relations.as_slice()
85    }
86}
87
88///
89/// EntityFieldDescription
90///
91/// One field-level projection inside `EntitySchemaDescription`.
92/// Keeps field type and queryability metadata explicit for diagnostics.
93///
94
95#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
96pub struct EntityFieldDescription {
97    pub(crate) name: String,
98    pub(crate) kind: String,
99    pub(crate) primary_key: bool,
100    pub(crate) queryable: bool,
101}
102
103impl EntityFieldDescription {
104    /// Construct one field description entry.
105    #[must_use]
106    pub const fn new(name: String, kind: String, primary_key: bool, queryable: bool) -> Self {
107        Self {
108            name,
109            kind,
110            primary_key,
111            queryable,
112        }
113    }
114
115    /// Borrow the field name.
116    #[must_use]
117    pub const fn name(&self) -> &str {
118        self.name.as_str()
119    }
120
121    /// Borrow the rendered field kind label.
122    #[must_use]
123    pub const fn kind(&self) -> &str {
124        self.kind.as_str()
125    }
126
127    /// Return whether this field is the primary key.
128    #[must_use]
129    pub const fn primary_key(&self) -> bool {
130        self.primary_key
131    }
132
133    /// Return whether this field is queryable.
134    #[must_use]
135    pub const fn queryable(&self) -> bool {
136        self.queryable
137    }
138}
139
140///
141/// EntityIndexDescription
142///
143/// One secondary-index projection inside `EntitySchemaDescription`.
144/// Includes uniqueness and ordered field list.
145///
146
147#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
148pub struct EntityIndexDescription {
149    pub(crate) name: String,
150    pub(crate) unique: bool,
151    pub(crate) fields: Vec<String>,
152}
153
154impl EntityIndexDescription {
155    /// Construct one index description entry.
156    #[must_use]
157    pub const fn new(name: String, unique: bool, fields: Vec<String>) -> Self {
158        Self {
159            name,
160            unique,
161            fields,
162        }
163    }
164
165    /// Borrow the index name.
166    #[must_use]
167    pub const fn name(&self) -> &str {
168        self.name.as_str()
169    }
170
171    /// Return whether the index enforces uniqueness.
172    #[must_use]
173    pub const fn unique(&self) -> bool {
174        self.unique
175    }
176
177    /// Borrow ordered index field names.
178    #[must_use]
179    pub const fn fields(&self) -> &[String] {
180        self.fields.as_slice()
181    }
182}
183
184///
185/// EntityRelationDescription
186///
187/// One relation-field projection inside `EntitySchemaDescription`.
188/// Captures relation target identity plus strength/cardinality metadata.
189///
190
191#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
192pub struct EntityRelationDescription {
193    pub(crate) field: String,
194    pub(crate) target_path: String,
195    pub(crate) target_entity_name: String,
196    pub(crate) target_store_path: String,
197    pub(crate) strength: EntityRelationStrength,
198    pub(crate) cardinality: EntityRelationCardinality,
199}
200
201impl EntityRelationDescription {
202    /// Construct one relation description entry.
203    #[must_use]
204    pub const fn new(
205        field: String,
206        target_path: String,
207        target_entity_name: String,
208        target_store_path: String,
209        strength: EntityRelationStrength,
210        cardinality: EntityRelationCardinality,
211    ) -> Self {
212        Self {
213            field,
214            target_path,
215            target_entity_name,
216            target_store_path,
217            strength,
218            cardinality,
219        }
220    }
221
222    /// Borrow the source relation field name.
223    #[must_use]
224    pub const fn field(&self) -> &str {
225        self.field.as_str()
226    }
227
228    /// Borrow the relation target path.
229    #[must_use]
230    pub const fn target_path(&self) -> &str {
231        self.target_path.as_str()
232    }
233
234    /// Borrow the relation target entity name.
235    #[must_use]
236    pub const fn target_entity_name(&self) -> &str {
237        self.target_entity_name.as_str()
238    }
239
240    /// Borrow the relation target store path.
241    #[must_use]
242    pub const fn target_store_path(&self) -> &str {
243        self.target_store_path.as_str()
244    }
245
246    /// Return relation strength.
247    #[must_use]
248    pub const fn strength(&self) -> EntityRelationStrength {
249        self.strength
250    }
251
252    /// Return relation cardinality.
253    #[must_use]
254    pub const fn cardinality(&self) -> EntityRelationCardinality {
255        self.cardinality
256    }
257}
258
259///
260/// EntityRelationStrength
261///
262/// Describe-surface relation strength projection.
263///
264#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
265pub enum EntityRelationStrength {
266    Strong,
267    Weak,
268}
269
270///
271/// EntityRelationCardinality
272///
273/// Describe-surface relation cardinality projection.
274///
275#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
276pub enum EntityRelationCardinality {
277    Single,
278    List,
279    Set,
280}
281
282/// Build one stable entity-schema description from one runtime `EntityModel`.
283#[must_use]
284pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
285    let mut fields = Vec::with_capacity(model.fields.len());
286    let mut relations = Vec::new();
287    for field in model.fields {
288        let field_kind = summarize_field_kind(&field.kind);
289        let queryable = field.kind.value_kind().is_queryable();
290        let primary_key = field.name == model.primary_key.name;
291
292        fields.push(EntityFieldDescription::new(
293            field.name.to_string(),
294            field_kind,
295            primary_key,
296            queryable,
297        ));
298
299        if let Some(relation) = relation_from_field_kind(field.name, &field.kind) {
300            relations.push(relation);
301        }
302    }
303
304    let mut indexes = Vec::with_capacity(model.indexes.len());
305    for index in model.indexes {
306        indexes.push(EntityIndexDescription::new(
307            index.name.to_string(),
308            index.unique,
309            index
310                .fields
311                .iter()
312                .map(|field| (*field).to_string())
313                .collect(),
314        ));
315    }
316
317    EntitySchemaDescription::new(
318        model.path.to_string(),
319        model.entity_name.to_string(),
320        model.primary_key.name.to_string(),
321        fields,
322        indexes,
323        relations,
324    )
325}
326
327// Resolve relation metadata from one field kind, including list/set relation forms.
328fn relation_from_field_kind(
329    field_name: &str,
330    kind: &FieldKind,
331) -> Option<EntityRelationDescription> {
332    match kind {
333        FieldKind::Relation {
334            target_path,
335            target_entity_name,
336            target_store_path,
337            strength,
338            ..
339        } => Some(EntityRelationDescription::new(
340            field_name.to_string(),
341            (*target_path).to_string(),
342            (*target_entity_name).to_string(),
343            (*target_store_path).to_string(),
344            relation_strength(*strength),
345            EntityRelationCardinality::Single,
346        )),
347        FieldKind::List(inner) => {
348            relation_from_collection_relation(field_name, inner, EntityRelationCardinality::List)
349        }
350        FieldKind::Set(inner) => {
351            relation_from_collection_relation(field_name, inner, EntityRelationCardinality::Set)
352        }
353        FieldKind::Account
354        | FieldKind::Blob
355        | FieldKind::Bool
356        | FieldKind::Date
357        | FieldKind::Decimal { .. }
358        | FieldKind::Duration
359        | FieldKind::Enum { .. }
360        | FieldKind::Float32
361        | FieldKind::Float64
362        | FieldKind::Int
363        | FieldKind::Int128
364        | FieldKind::IntBig
365        | FieldKind::Principal
366        | FieldKind::Subaccount
367        | FieldKind::Text
368        | FieldKind::Timestamp
369        | FieldKind::Uint
370        | FieldKind::Uint128
371        | FieldKind::UintBig
372        | FieldKind::Ulid
373        | FieldKind::Unit
374        | FieldKind::Map { .. }
375        | FieldKind::Structured { .. } => None,
376    }
377}
378
379// Resolve list/set relation metadata only when the collection inner shape is relation.
380fn relation_from_collection_relation(
381    field_name: &str,
382    inner: &FieldKind,
383    cardinality: EntityRelationCardinality,
384) -> Option<EntityRelationDescription> {
385    let FieldKind::Relation {
386        target_path,
387        target_entity_name,
388        target_store_path,
389        strength,
390        ..
391    } = inner
392    else {
393        return None;
394    };
395
396    Some(EntityRelationDescription::new(
397        field_name.to_string(),
398        (*target_path).to_string(),
399        (*target_entity_name).to_string(),
400        (*target_store_path).to_string(),
401        relation_strength(*strength),
402        cardinality,
403    ))
404}
405
406// Project runtime relation strength into the describe DTO surface.
407const fn relation_strength(strength: RelationStrength) -> EntityRelationStrength {
408    match strength {
409        RelationStrength::Strong => EntityRelationStrength::Strong,
410        RelationStrength::Weak => EntityRelationStrength::Weak,
411    }
412}
413
414// Render one stable field-kind label for describe output.
415fn summarize_field_kind(kind: &FieldKind) -> String {
416    match kind {
417        FieldKind::Account => "account".to_string(),
418        FieldKind::Blob => "blob".to_string(),
419        FieldKind::Bool => "bool".to_string(),
420        FieldKind::Date => "date".to_string(),
421        FieldKind::Decimal { scale } => format!("decimal(scale={scale})"),
422        FieldKind::Duration => "duration".to_string(),
423        FieldKind::Enum { path } => format!("enum({path})"),
424        FieldKind::Float32 => "float32".to_string(),
425        FieldKind::Float64 => "float64".to_string(),
426        FieldKind::Int => "int".to_string(),
427        FieldKind::Int128 => "int128".to_string(),
428        FieldKind::IntBig => "int_big".to_string(),
429        FieldKind::Principal => "principal".to_string(),
430        FieldKind::Subaccount => "subaccount".to_string(),
431        FieldKind::Text => "text".to_string(),
432        FieldKind::Timestamp => "timestamp".to_string(),
433        FieldKind::Uint => "uint".to_string(),
434        FieldKind::Uint128 => "uint128".to_string(),
435        FieldKind::UintBig => "uint_big".to_string(),
436        FieldKind::Ulid => "ulid".to_string(),
437        FieldKind::Unit => "unit".to_string(),
438        FieldKind::Relation {
439            target_entity_name,
440            key_kind,
441            strength,
442            ..
443        } => format!(
444            "relation(target={target_entity_name}, key={}, strength={})",
445            summarize_field_kind(key_kind),
446            summarize_relation_strength(*strength),
447        ),
448        FieldKind::List(inner) => format!("list<{}>", summarize_field_kind(inner)),
449        FieldKind::Set(inner) => format!("set<{}>", summarize_field_kind(inner)),
450        FieldKind::Map { key, value } => {
451            format!(
452                "map<{}, {}>",
453                summarize_field_kind(key),
454                summarize_field_kind(value)
455            )
456        }
457        FieldKind::Structured { queryable } => format!("structured(queryable={queryable})"),
458    }
459}
460
461// Render one stable relation-strength label for field-kind summaries.
462const fn summarize_relation_strength(strength: RelationStrength) -> &'static str {
463    match strength {
464        RelationStrength::Strong => "strong",
465        RelationStrength::Weak => "weak",
466    }
467}