Skip to main content

icydb_core/db/session/
catalog.rs

1//! Module: db::session::catalog
2//! Responsibility: session-owned catalog, schema-description, and storage
3//! observability surfaces.
4//! Does not own: schema reconciliation policy, query planning, or storage
5//! mutation.
6//! Boundary: converts accepted/generated schema authority into stable
7//! introspection DTOs at the session facade.
8
9#[cfg(any(test, feature = "sql-explain"))]
10use crate::db::{IndexState, QueryError, query::plan::VisibleIndexes};
11use crate::{
12    db::{
13        DbSession, EntityCatalogCounts, EntityCatalogDescription, EntityConstraintDescription,
14        EntityFieldDescription, EntitySchemaDescription, StorageReport, StoreCatalogDescription,
15        schema::{
16            AcceptedFieldKind, ConstraintValidationJob, PersistedFieldSnapshot, SchemaInfo,
17            describe_entity_fields, describe_entity_fields_with_persisted_schema,
18            describe_entity_model, describe_entity_model_with_persisted_schema,
19            show_indexes_for_model, show_indexes_for_model_with_runtime_state,
20            show_indexes_for_schema_info_with_runtime_state,
21        },
22    },
23    entity::EntityKind,
24    error::InternalError,
25    model::entity::EntityModel,
26    traits::{CanisterKind, Path},
27};
28
29fn relation_field_count(fields: &[PersistedFieldSnapshot]) -> usize {
30    fields
31        .iter()
32        .filter(|field| persisted_kind_is_relation_field(field.kind()))
33        .count()
34}
35
36fn persisted_kind_is_relation_field(kind: &AcceptedFieldKind) -> bool {
37    match kind {
38        AcceptedFieldKind::Relation { .. } => true,
39        AcceptedFieldKind::List(inner) | AcceptedFieldKind::Set(inner) => {
40            matches!(inner.as_ref(), AcceptedFieldKind::Relation { .. })
41        }
42        _ => false,
43    }
44}
45
46impl<C: CanisterKind> DbSession<C> {
47    /// Return one stable, human-readable index listing for the entity schema.
48    ///
49    /// Output format mirrors SQL-style introspection:
50    /// - `PRIMARY KEY (field) [state=ready] [origin=generated]`
51    /// - `INDEX name (field_a, field_b) [state=ready] [origin=generated]`
52    /// - `UNIQUE INDEX name (field_a, field_b) [state=ready] [origin=generated]`
53    #[must_use]
54    pub fn show_indexes<E>(&self) -> Vec<String>
55    where
56        E: EntityKind<Canister = C>,
57    {
58        self.show_indexes_for_store_model(E::Store::PATH, E::MODEL)
59    }
60
61    /// Return one stable, human-readable index listing for one schema model.
62    ///
63    /// This model-only helper is schema-owned and intentionally does not
64    /// attach runtime lifecycle state because it does not carry store
65    /// placement context.
66    #[must_use]
67    pub fn show_indexes_for_model(&self, model: &'static EntityModel) -> Vec<String> {
68        show_indexes_for_model(model)
69    }
70
71    /// Return one stable, human-readable index listing for the accepted schema.
72    ///
73    /// Unlike `show_indexes`, this fallible live-schema helper reflects
74    /// accepted DDL-created indexes as well as compiled schema indexes.
75    pub fn try_show_indexes<E>(&self) -> Result<Vec<String>, InternalError>
76    where
77        E: EntityKind<Canister = C>,
78    {
79        let schema = self.accepted_schema_info_for_entity::<E>()?;
80
81        Ok(self.show_indexes_for_store_schema_info(E::Store::PATH, &schema))
82    }
83
84    /// Return accepted structural constraints ordered by stable identity.
85    pub fn try_show_constraints<E>(&self) -> Result<Vec<EntityConstraintDescription>, InternalError>
86    where
87        E: EntityKind<Canister = C>,
88    {
89        Ok(self.try_describe_entity::<E>()?.constraints().to_vec())
90    }
91
92    // Return one stable, human-readable index listing for one resolved
93    // store/model pair, attaching the current runtime lifecycle state when the
94    // registry can resolve the backing store handle.
95    pub(in crate::db) fn show_indexes_for_store_model(
96        &self,
97        store_path: &str,
98        model: &'static EntityModel,
99    ) -> Vec<String> {
100        let runtime_state = self
101            .db
102            .with_store_registry(|registry| registry.try_get_store(store_path).ok())
103            .map(|store| store.index_state());
104
105        show_indexes_for_model_with_runtime_state(model, runtime_state)
106    }
107
108    // Return one stable, human-readable index listing for one resolved
109    // store/accepted-schema pair, attaching the current runtime lifecycle state
110    // when the registry can resolve the backing store handle.
111    pub(in crate::db) fn show_indexes_for_store_schema_info(
112        &self,
113        store_path: &str,
114        schema: &SchemaInfo,
115    ) -> Vec<String> {
116        let runtime_state = self
117            .db
118            .with_store_registry(|registry| registry.try_get_store(store_path).ok())
119            .map(|store| store.index_state());
120
121        show_indexes_for_schema_info_with_runtime_state(schema, runtime_state)
122    }
123
124    /// Return one stable generated-model list of field descriptors.
125    ///
126    /// This infallible Rust metadata helper intentionally reports the compiled
127    /// schema model. Use `try_show_columns` for the accepted persisted-schema
128    /// view used by SQL and diagnostics surfaces.
129    #[must_use]
130    pub fn show_columns<E>(&self) -> Vec<EntityFieldDescription>
131    where
132        E: EntityKind<Canister = C>,
133    {
134        self.show_columns_for_model(E::MODEL)
135    }
136
137    /// Return one stable generated-model list of field descriptors.
138    #[must_use]
139    pub fn show_columns_for_model(
140        &self,
141        model: &'static EntityModel,
142    ) -> Vec<EntityFieldDescription> {
143        describe_entity_fields(model)
144    }
145
146    /// Return field descriptors using the accepted persisted schema snapshot.
147    ///
148    /// This fallible variant is intended for SQL and diagnostics surfaces that
149    /// can report schema reconciliation failures. The infallible
150    /// `show_columns` helper remains generated-model based.
151    pub fn try_show_columns<E>(&self) -> Result<Vec<EntityFieldDescription>, InternalError>
152    where
153        E: EntityKind<Canister = C>,
154    {
155        let catalog = self.accepted_schema_catalog_context_for_query::<E>()?;
156
157        describe_entity_fields_with_persisted_schema(
158            catalog.snapshot(),
159            catalog.value_catalog_handle(),
160        )
161    }
162
163    /// Return one stable list of runtime-registered entity catalog entries.
164    ///
165    /// # Panics
166    ///
167    /// Panics if the runtime session cannot read its registered entity catalog.
168    /// Use `try_show_entities` when the caller can report the internal error.
169    #[must_use]
170    pub fn show_entities(&self) -> Vec<crate::db::EntityCatalogDescription> {
171        self.try_show_entities().expect("session invariant")
172    }
173
174    /// Return one stable list of runtime-registered entity catalog entries.
175    pub fn try_show_entities(&self) -> Result<Vec<EntityCatalogDescription>, InternalError> {
176        let mut entities = Vec::with_capacity(self.db.entity_runtime_hooks.len());
177
178        for hooks in self.db.entity_runtime_hooks {
179            let store = self.db.recovered_store(hooks.store_path)?;
180            let storage = store
181                .storage_capabilities()
182                .storage_mode()
183                .as_str()
184                .to_string();
185            let accepted = self.accepted_schema_catalog_context_for_runtime_hook(hooks, store)?;
186            let snapshot = accepted.snapshot().persisted_snapshot();
187
188            entities.push(EntityCatalogDescription::new(
189                snapshot.entity_name().to_string(),
190                snapshot.entity_path().to_string(),
191                hooks.store_path.to_string(),
192                storage,
193                EntityCatalogCounts::new(
194                    u32::try_from(snapshot.fields().len()).unwrap_or(u32::MAX),
195                    u32::try_from(snapshot.indexes().len()).unwrap_or(u32::MAX),
196                    u32::try_from(relation_field_count(snapshot.fields())).unwrap_or(u32::MAX),
197                    snapshot.version().get(),
198                ),
199            ));
200        }
201
202        Ok(entities)
203    }
204
205    /// Return one stable list of runtime-registered stores.
206    #[must_use]
207    pub fn show_stores(&self) -> Vec<StoreCatalogDescription> {
208        self.db.runtime_store_catalog()
209    }
210
211    /// Return one stable list of runtime-registered stable-memory allocations.
212    #[must_use]
213    pub fn show_memory(&self) -> Vec<crate::db::MemoryCatalogDescription> {
214        self.db.runtime_memory_catalog()
215    }
216
217    // Resolve the exact secondary-index set that is visible to planner-owned
218    // query planning for one recovered store and accepted schema pair.
219    #[cfg(any(test, feature = "sql-explain"))]
220    pub(in crate::db::session) fn visible_indexes_for_store_accepted_schema(
221        &self,
222        store_path: &str,
223        schema_info: &SchemaInfo,
224    ) -> Result<VisibleIndexes<'static>, QueryError> {
225        // Phase 1: resolve the recovered store state once at the session
226        // boundary so query/executor planning does not reopen lifecycle checks.
227        let store = self
228            .db
229            .recovered_store(store_path)
230            .map_err(QueryError::execute)?;
231        let state = store.index_state();
232        if state != IndexState::Ready {
233            return Ok(VisibleIndexes::none());
234        }
235        debug_assert_eq!(state, IndexState::Ready);
236
237        // Phase 2: planner-visible indexes are accepted schema contracts once
238        // the recovered store is query-visible.
239        let visible_indexes = VisibleIndexes::accepted_schema_visible(schema_info);
240        debug_assert!(visible_indexes.accepted_field_path_contracts_are_consistent());
241        debug_assert!(visible_indexes.accepted_expression_contracts_are_consistent());
242        debug_assert_eq!(
243            visible_indexes.accepted_expression_index_count(),
244            Some(visible_indexes.accepted_expression_indexes().len()),
245        );
246
247        Ok(visible_indexes)
248    }
249
250    /// Return one generated-model schema description for the entity.
251    ///
252    /// This is a typed `DESCRIBE`-style introspection surface consumed by
253    /// developer tooling and pre-EXPLAIN debugging when a non-failing compiled
254    /// schema view is required.
255    #[must_use]
256    pub fn describe_entity<E>(&self) -> EntitySchemaDescription
257    where
258        E: EntityKind<Canister = C>,
259    {
260        self.describe_entity_model(E::MODEL)
261    }
262
263    /// Return one generated-model schema description for one schema model.
264    #[must_use]
265    pub fn describe_entity_model(&self, model: &'static EntityModel) -> EntitySchemaDescription {
266        describe_entity_model(model)
267    }
268
269    /// Return a schema description using the accepted persisted schema snapshot.
270    ///
271    /// This is the live-schema counterpart to `describe_entity`. It is fallible
272    /// because loading accepted schema authority can fail if startup
273    /// reconciliation rejects the stored metadata.
274    pub fn try_describe_entity<E>(&self) -> Result<EntitySchemaDescription, InternalError>
275    where
276        E: EntityKind<Canister = C>,
277    {
278        let catalog = self.accepted_schema_catalog_context_for_query::<E>()?;
279        let validation_jobs = self.constraint_validation_jobs_for_catalog::<E>(&catalog)?;
280
281        describe_entity_model_with_persisted_schema(
282            E::MODEL,
283            catalog.snapshot(),
284            catalog.value_catalog_handle(),
285            validation_jobs.as_slice(),
286        )
287    }
288
289    pub(in crate::db::session) fn constraint_validation_jobs_for_catalog<E>(
290        &self,
291        catalog: &crate::db::session::AcceptedSchemaCatalogContext,
292    ) -> Result<Vec<ConstraintValidationJob>, InternalError>
293    where
294        E: EntityKind<Canister = C>,
295    {
296        let store = self.db.recovered_store(E::Store::PATH)?;
297        store.with_schema(|schema_store| {
298            let jobs = catalog
299                .snapshot()
300                .persisted_snapshot()
301                .constraint_activations()
302                .iter()
303                .map(|activation| {
304                    schema_store.constraint_validation_job(E::ENTITY_TAG, activation.id())
305                })
306                .collect::<Result<Vec<_>, InternalError>>()?;
307            jobs.into_iter()
308                .flatten()
309                .map(|job| {
310                    if job.entity_tag() != E::ENTITY_TAG
311                        || job.entity_path() != catalog.snapshot().entity_path()
312                    {
313                        return Err(InternalError::store_invariant());
314                    }
315                    Ok(job)
316                })
317                .collect()
318        })
319    }
320
321    /// Build one point-in-time storage report for observability endpoints.
322    pub fn storage_report(
323        &self,
324        name_to_path: &[(&'static str, &'static str)],
325    ) -> Result<StorageReport, InternalError> {
326        self.db.storage_report(name_to_path)
327    }
328
329    /// Build one point-in-time storage report using default entity-path labels.
330    pub fn storage_report_default(&self) -> Result<StorageReport, InternalError> {
331        self.db.storage_report_default()
332    }
333}