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