Skip to main content

icydb_core/db/registry/
handle.rs

1//! Module: db::registry::handle
2//! Responsibility: stable store handles and runtime storage capability descriptors.
3//! Does not own: registry path lookup or store mutation semantics.
4//! Boundary: exposes registered storage roles without exposing registry internals.
5
6use crate::db::{
7    data::DataStore,
8    index::{IndexState, IndexStore},
9    journal::JournalTailStore,
10    schema::SchemaStore,
11};
12use candid::CandidType;
13use serde::Deserialize;
14use std::{cell::RefCell, thread::LocalKey};
15
16///
17/// StoreHandle
18///
19/// StoreHandle binds the row, index, and schema stores for one generated schema
20/// `Store` path.
21/// It is the stable access token passed across commit, recovery, executor, and
22/// diagnostics boundaries instead of exposing registry internals directly.
23///
24
25#[derive(Clone, Copy, Debug)]
26pub struct StoreHandle {
27    data: &'static LocalKey<RefCell<DataStore>>,
28    index: &'static LocalKey<RefCell<IndexStore>>,
29    schema: &'static LocalKey<RefCell<SchemaStore>>,
30    journal: Option<&'static LocalKey<RefCell<JournalTailStore>>>,
31    allocations: StoreAllocationIdentities,
32    capabilities: StoreRuntimeStorageCapabilities,
33}
34
35/// Diagnostic storage mode carried by a runtime storage capability descriptor.
36///
37/// Policy code should branch on capability axes instead of this display value.
38#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
39pub enum StoreRuntimeStorageMode {
40    /// Volatile in-process heap storage.
41    #[default]
42    Heap,
43    /// Journaled cached-stable durable storage.
44    Journaled,
45}
46
47impl StoreRuntimeStorageMode {
48    /// Return the user-facing storage mode label.
49    #[must_use]
50    pub const fn as_str(self) -> &'static str {
51        match self {
52            Self::Heap => "heap",
53            Self::Journaled => "journaled",
54        }
55    }
56}
57
58/// Whether a store owns durable allocation identity.
59#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
60pub enum StoreAllocationIdentityCapability {
61    /// Stable allocation identity is present.
62    #[default]
63    Present,
64    /// Stable allocation identity is absent.
65    Absent,
66}
67
68impl StoreAllocationIdentityCapability {
69    /// Return the user-facing capability label.
70    #[must_use]
71    pub const fn as_str(self) -> &'static str {
72        match self {
73            Self::Present => "present",
74            Self::Absent => "absent",
75        }
76    }
77}
78
79/// Store durability class.
80#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
81pub enum StoreDurability {
82    /// Store contents participate in durable storage semantics.
83    #[default]
84    Durable,
85    /// Store contents are live-only and volatile.
86    Volatile,
87}
88
89impl StoreDurability {
90    /// Return the user-facing durability label.
91    #[must_use]
92    pub const fn as_str(self) -> &'static str {
93        match self {
94            Self::Durable => "durable",
95            Self::Volatile => "volatile",
96        }
97    }
98}
99
100/// Store recovery capability.
101#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
102pub enum StoreRecoveryCapability {
103    /// Store contents can be recovered from canonical stable BTrees plus a
104    /// committed journal tail.
105    #[default]
106    StableBasePlusJournalReplay,
107    /// Store contents are not recovered.
108    None,
109}
110
111impl StoreRecoveryCapability {
112    /// Return the user-facing recovery label.
113    #[must_use]
114    pub const fn as_str(self) -> &'static str {
115        match self {
116            Self::StableBasePlusJournalReplay => "stable-base-plus-journal-replay",
117            Self::None => "none",
118        }
119    }
120}
121
122/// Store commit participation class.
123#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
124pub enum StoreCommitParticipation {
125    /// Store mutations participate in the durable commit path.
126    #[default]
127    Durable,
128    /// Store mutations are live-only side effects.
129    LiveOnly,
130}
131
132impl StoreCommitParticipation {
133    /// Return the user-facing commit-participation label.
134    #[must_use]
135    pub const fn as_str(self) -> &'static str {
136        match self {
137            Self::Durable => "durable",
138            Self::LiveOnly => "live-only",
139        }
140    }
141}
142
143/// Store schema metadata persistence class.
144#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
145pub enum StoreSchemaMetadataCapability {
146    /// The store-local projection is rebuilt from a durable accepted checkpoint
147    /// and does not retain its own schema history.
148    LiveRebuiltMetadata,
149    /// Schema metadata is canonical stable history plus committed journal tail.
150    #[default]
151    CanonicalStableHistoryPlusJournalTail,
152}
153
154impl StoreSchemaMetadataCapability {
155    /// Return the user-facing schema-metadata capability label.
156    #[must_use]
157    pub const fn as_str(self) -> &'static str {
158        match self {
159            Self::LiveRebuiltMetadata => "live-rebuilt-metadata",
160            Self::CanonicalStableHistoryPlusJournalTail => {
161                "canonical-stable-history-plus-journal-tail"
162            }
163        }
164    }
165}
166
167/// Relation source capability for a store.
168#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
169pub enum StoreRelationSourceCapability {
170    /// Source rows can own durable relation integrity.
171    #[default]
172    DurableSource,
173    /// Source rows can participate in live relation validation.
174    LiveSource,
175}
176
177/// Relation target capability for a store.
178#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
179pub enum StoreRelationTargetCapability {
180    /// Target rows can be referenced by durable source rows.
181    #[default]
182    DurableTarget,
183    /// Target rows are volatile and cannot satisfy durable source integrity.
184    VolatileTarget,
185}
186
187/// Runtime storage capability descriptor carried by one registered store.
188///
189/// Capabilities describe storage policy. They are not allocation identity.
190#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
191pub struct StoreRuntimeStorageCapabilities {
192    storage_mode: StoreRuntimeStorageMode,
193    allocation_identity: StoreAllocationIdentityCapability,
194    durability: StoreDurability,
195    recovery: StoreRecoveryCapability,
196    commit_participation: StoreCommitParticipation,
197    schema_metadata: StoreSchemaMetadataCapability,
198    relation_source: StoreRelationSourceCapability,
199    relation_target: StoreRelationTargetCapability,
200}
201
202impl StoreRuntimeStorageCapabilities {
203    /// Capability descriptor for heap stores.
204    #[must_use]
205    pub const fn heap() -> Self {
206        Self {
207            storage_mode: StoreRuntimeStorageMode::Heap,
208            allocation_identity: StoreAllocationIdentityCapability::Absent,
209            durability: StoreDurability::Volatile,
210            recovery: StoreRecoveryCapability::None,
211            commit_participation: StoreCommitParticipation::LiveOnly,
212            schema_metadata: StoreSchemaMetadataCapability::LiveRebuiltMetadata,
213            relation_source: StoreRelationSourceCapability::LiveSource,
214            relation_target: StoreRelationTargetCapability::VolatileTarget,
215        }
216    }
217
218    /// Capability descriptor for journaled cached-stable stores.
219    #[must_use]
220    pub const fn journaled() -> Self {
221        Self {
222            storage_mode: StoreRuntimeStorageMode::Journaled,
223            allocation_identity: StoreAllocationIdentityCapability::Present,
224            durability: StoreDurability::Durable,
225            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
226            commit_participation: StoreCommitParticipation::Durable,
227            schema_metadata: StoreSchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
228            relation_source: StoreRelationSourceCapability::DurableSource,
229            relation_target: StoreRelationTargetCapability::DurableTarget,
230        }
231    }
232
233    /// Diagnostic storage mode. Policy code should use the capability axes.
234    #[must_use]
235    pub const fn storage_mode(self) -> StoreRuntimeStorageMode {
236        self.storage_mode
237    }
238
239    /// Allocation identity capability.
240    #[must_use]
241    pub const fn allocation_identity(self) -> StoreAllocationIdentityCapability {
242        self.allocation_identity
243    }
244
245    /// Durability capability.
246    #[must_use]
247    pub const fn durability(self) -> StoreDurability {
248        self.durability
249    }
250
251    /// Recovery capability.
252    #[must_use]
253    pub const fn recovery(self) -> StoreRecoveryCapability {
254        self.recovery
255    }
256
257    /// Commit participation capability.
258    #[must_use]
259    pub const fn commit_participation(self) -> StoreCommitParticipation {
260        self.commit_participation
261    }
262
263    /// Schema metadata persistence capability.
264    #[must_use]
265    pub const fn schema_metadata(self) -> StoreSchemaMetadataCapability {
266        self.schema_metadata
267    }
268
269    /// Relation source capability.
270    #[must_use]
271    pub const fn relation_source(self) -> StoreRelationSourceCapability {
272        self.relation_source
273    }
274
275    /// Relation target capability.
276    #[must_use]
277    pub const fn relation_target(self) -> StoreRelationTargetCapability {
278        self.relation_target
279    }
280}
281
282///
283/// StoreAllocationIdentity
284///
285/// Durable allocation identity for one physical stable-memory role.
286///
287
288#[derive(Clone, Copy, Debug, Eq, PartialEq)]
289pub struct StoreAllocationIdentity {
290    memory_id: u8,
291    stable_key: &'static str,
292}
293
294impl StoreAllocationIdentity {
295    /// Build one stable allocation identity descriptor.
296    #[must_use]
297    pub const fn new(memory_id: u8, stable_key: &'static str) -> Self {
298        Self {
299            memory_id,
300            stable_key,
301        }
302    }
303
304    /// Stable-memory manager ID.
305    #[must_use]
306    pub const fn memory_id(self) -> u8 {
307        self.memory_id
308    }
309
310    /// Durable stable-memory key.
311    #[must_use]
312    pub const fn stable_key(self) -> &'static str {
313        self.stable_key
314    }
315}
316
317///
318/// StoreAllocationIdentities
319///
320/// Durable allocation identities for one logical store's data, index, and
321/// schema memories.
322///
323
324#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
325pub struct StoreAllocationIdentities {
326    data: Option<StoreAllocationIdentity>,
327    index: Option<StoreAllocationIdentity>,
328    schema: Option<StoreAllocationIdentity>,
329    journal: Option<StoreAllocationIdentity>,
330}
331
332impl StoreAllocationIdentities {
333    /// Build an absent allocation identity bundle.
334    #[must_use]
335    pub const fn absent() -> Self {
336        Self {
337            data: None,
338            index: None,
339            schema: None,
340            journal: None,
341        }
342    }
343
344    /// Build one journaled cached-stable allocation identity bundle.
345    #[must_use]
346    pub const fn new_journaled(
347        data: StoreAllocationIdentity,
348        index: StoreAllocationIdentity,
349        schema: StoreAllocationIdentity,
350        journal: StoreAllocationIdentity,
351    ) -> Self {
352        Self {
353            data: Some(data),
354            index: Some(index),
355            schema: Some(schema),
356            journal: Some(journal),
357        }
358    }
359
360    /// Return data-memory allocation identity.
361    #[must_use]
362    pub const fn data(self) -> Option<StoreAllocationIdentity> {
363        self.data
364    }
365
366    /// Return index-memory allocation identity.
367    #[must_use]
368    pub const fn index(self) -> Option<StoreAllocationIdentity> {
369        self.index
370    }
371
372    /// Return schema-memory allocation identity.
373    #[must_use]
374    pub const fn schema(self) -> Option<StoreAllocationIdentity> {
375        self.schema
376    }
377
378    /// Return journal-tail allocation identity.
379    #[must_use]
380    pub const fn journal(self) -> Option<StoreAllocationIdentity> {
381        self.journal
382    }
383
384    /// Return the allocation capability represented by this triplet, or
385    /// `None` if the triplet is partially populated and therefore invalid.
386    #[must_use]
387    pub const fn allocation_identity_capability(self) -> Option<StoreAllocationIdentityCapability> {
388        match (self.data, self.index, self.schema) {
389            (Some(_), Some(_), Some(_)) => Some(StoreAllocationIdentityCapability::Present),
390            (None, None, None) if self.journal.is_none() => {
391                Some(StoreAllocationIdentityCapability::Absent)
392            }
393            _ => None,
394        }
395    }
396
397    /// Return whether this allocation shape matches the concrete storage
398    /// capability descriptor.
399    #[must_use]
400    pub const fn matches_storage_capabilities(
401        self,
402        capabilities: StoreRuntimeStorageCapabilities,
403    ) -> bool {
404        match capabilities.storage_mode() {
405            StoreRuntimeStorageMode::Heap => {
406                self.data.is_none()
407                    && self.index.is_none()
408                    && self.schema.is_none()
409                    && self.journal.is_none()
410            }
411            StoreRuntimeStorageMode::Journaled => {
412                self.data.is_some()
413                    && self.index.is_some()
414                    && self.schema.is_some()
415                    && self.journal.is_some()
416            }
417        }
418    }
419}
420
421impl StoreHandle {
422    /// Build a store handle with an explicit allocation identity decision.
423    #[must_use]
424    pub const fn new(
425        data: &'static LocalKey<RefCell<DataStore>>,
426        index: &'static LocalKey<RefCell<IndexStore>>,
427        schema: &'static LocalKey<RefCell<SchemaStore>>,
428        allocations: StoreAllocationIdentities,
429        capabilities: StoreRuntimeStorageCapabilities,
430    ) -> Self {
431        Self {
432            data,
433            index,
434            schema,
435            journal: None,
436            allocations,
437            capabilities,
438        }
439    }
440
441    /// Build a journaled store handle with an explicit journal-tail store.
442    #[must_use]
443    pub const fn new_journaled(
444        data: &'static LocalKey<RefCell<DataStore>>,
445        index: &'static LocalKey<RefCell<IndexStore>>,
446        schema: &'static LocalKey<RefCell<SchemaStore>>,
447        journal: &'static LocalKey<RefCell<JournalTailStore>>,
448        allocations: StoreAllocationIdentities,
449        capabilities: StoreRuntimeStorageCapabilities,
450    ) -> Self {
451        Self {
452            data,
453            index,
454            schema,
455            journal: Some(journal),
456            allocations,
457            capabilities,
458        }
459    }
460
461    /// Borrow the row store immutably.
462    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
463        #[cfg(feature = "diagnostics")]
464        {
465            crate::db::physical_access::measure_physical_access_operation(|| {
466                self.data.with_borrow(f)
467            })
468        }
469
470        #[cfg(not(feature = "diagnostics"))]
471        {
472            self.data.with_borrow(f)
473        }
474    }
475
476    /// Borrow the row store mutably.
477    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
478        self.data.with_borrow_mut(f)
479    }
480
481    /// Borrow the index store immutably.
482    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
483        #[cfg(feature = "diagnostics")]
484        {
485            crate::db::physical_access::measure_physical_access_operation(|| {
486                self.index.with_borrow(f)
487            })
488        }
489
490        #[cfg(not(feature = "diagnostics"))]
491        {
492            self.index.with_borrow(f)
493        }
494    }
495
496    /// Borrow the index store mutably.
497    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
498        self.index.with_borrow_mut(f)
499    }
500
501    /// Borrow the schema store immutably.
502    pub fn with_schema<R>(&self, f: impl FnOnce(&SchemaStore) -> R) -> R {
503        self.schema.with_borrow(f)
504    }
505
506    /// Borrow the schema store mutably.
507    pub fn with_schema_mut<R>(&self, f: impl FnOnce(&mut SchemaStore) -> R) -> R {
508        self.schema.with_borrow_mut(f)
509    }
510
511    /// Return the explicit lifecycle state of the bound index store.
512    #[must_use]
513    pub(in crate::db) fn index_state(&self) -> IndexState {
514        self.with_index(IndexStore::state)
515    }
516
517    /// Mark the bound index store as Building.
518    pub(in crate::db) fn mark_index_building(&self) {
519        self.with_index_mut(IndexStore::mark_building);
520    }
521
522    /// Mark the bound index store as Ready.
523    pub(in crate::db) fn mark_index_ready(&self) {
524        self.with_index_mut(IndexStore::mark_ready);
525    }
526
527    /// Return the raw row-store accessor.
528    #[must_use]
529    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
530        self.data
531    }
532
533    /// Return the raw index-store accessor.
534    #[must_use]
535    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
536        self.index
537    }
538
539    /// Return the raw schema-store accessor.
540    #[must_use]
541    pub const fn schema_store(&self) -> &'static LocalKey<RefCell<SchemaStore>> {
542        self.schema
543    }
544
545    /// Return the raw journal-tail store accessor when this store is journaled.
546    #[must_use]
547    pub const fn journal_tail_store(&self) -> Option<&'static LocalKey<RefCell<JournalTailStore>>> {
548        self.journal
549    }
550
551    /// Return the data-memory allocation identity when generated wiring
552    /// supplied it.
553    #[must_use]
554    pub const fn data_allocation(&self) -> Option<StoreAllocationIdentity> {
555        self.allocations.data()
556    }
557
558    /// Return the index-memory allocation identity when generated wiring
559    /// supplied it.
560    #[must_use]
561    pub const fn index_allocation(&self) -> Option<StoreAllocationIdentity> {
562        self.allocations.index()
563    }
564
565    /// Return the schema-memory allocation identity when generated wiring
566    /// supplied it.
567    #[must_use]
568    pub const fn schema_allocation(&self) -> Option<StoreAllocationIdentity> {
569        self.allocations.schema()
570    }
571
572    /// Return the journal-tail allocation identity when generated wiring
573    /// supplied it.
574    #[must_use]
575    pub const fn journal_allocation(&self) -> Option<StoreAllocationIdentity> {
576        self.allocations.journal()
577    }
578
579    /// Return this store's complete allocation identity bundle.
580    #[must_use]
581    pub(in crate::db) const fn allocation_identities(&self) -> StoreAllocationIdentities {
582        self.allocations
583    }
584
585    /// Return this store's explicit runtime storage capabilities.
586    #[must_use]
587    pub const fn storage_capabilities(&self) -> StoreRuntimeStorageCapabilities {
588        self.capabilities
589    }
590}