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