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    /// Schema metadata is rebuilt live and is not durable history.
147    LiveRebuiltMetadata,
148    /// Schema metadata is canonical stable history plus committed journal tail.
149    #[default]
150    CanonicalStableHistoryPlusJournalTail,
151}
152
153impl StoreSchemaMetadataCapability {
154    /// Return the user-facing schema-metadata capability label.
155    #[must_use]
156    pub const fn as_str(self) -> &'static str {
157        match self {
158            Self::LiveRebuiltMetadata => "live-rebuilt-metadata",
159            Self::CanonicalStableHistoryPlusJournalTail => {
160                "canonical-stable-history-plus-journal-tail"
161            }
162        }
163    }
164}
165
166/// Strong relation source capability for a store.
167#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
168pub enum StoreRelationSourceCapability {
169    /// Source rows can own durable relation integrity.
170    #[default]
171    DurableSource,
172    /// Source rows can participate in live relation validation.
173    LiveSource,
174}
175
176/// Strong relation target capability for a store.
177#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
178pub enum StoreRelationTargetCapability {
179    /// Target rows can be referenced by durable source rows.
180    #[default]
181    DurableTarget,
182    /// Target rows are volatile and cannot satisfy durable source integrity.
183    VolatileTarget,
184}
185
186/// Whether the store can participate in live validation.
187#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
188pub enum StoreLiveValidationCapability {
189    /// Live validation is supported.
190    #[default]
191    Supported,
192}
193
194/// Runtime storage capability descriptor carried by one registered store.
195///
196/// Capabilities describe storage policy. They are not allocation identity.
197#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
198pub struct StoreRuntimeStorageCapabilities {
199    storage_mode: StoreRuntimeStorageMode,
200    allocation_identity: StoreAllocationIdentityCapability,
201    durability: StoreDurability,
202    recovery: StoreRecoveryCapability,
203    commit_participation: StoreCommitParticipation,
204    schema_metadata: StoreSchemaMetadataCapability,
205    relation_source: StoreRelationSourceCapability,
206    relation_target: StoreRelationTargetCapability,
207    live_validation: StoreLiveValidationCapability,
208}
209
210impl StoreRuntimeStorageCapabilities {
211    /// Capability descriptor for heap stores.
212    #[must_use]
213    pub const fn heap() -> Self {
214        Self {
215            storage_mode: StoreRuntimeStorageMode::Heap,
216            allocation_identity: StoreAllocationIdentityCapability::Absent,
217            durability: StoreDurability::Volatile,
218            recovery: StoreRecoveryCapability::None,
219            commit_participation: StoreCommitParticipation::LiveOnly,
220            schema_metadata: StoreSchemaMetadataCapability::LiveRebuiltMetadata,
221            relation_source: StoreRelationSourceCapability::LiveSource,
222            relation_target: StoreRelationTargetCapability::VolatileTarget,
223            live_validation: StoreLiveValidationCapability::Supported,
224        }
225    }
226
227    /// Capability descriptor for journaled cached-stable stores.
228    #[must_use]
229    pub const fn journaled() -> Self {
230        Self {
231            storage_mode: StoreRuntimeStorageMode::Journaled,
232            allocation_identity: StoreAllocationIdentityCapability::Present,
233            durability: StoreDurability::Durable,
234            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
235            commit_participation: StoreCommitParticipation::Durable,
236            schema_metadata: StoreSchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
237            relation_source: StoreRelationSourceCapability::DurableSource,
238            relation_target: StoreRelationTargetCapability::DurableTarget,
239            live_validation: StoreLiveValidationCapability::Supported,
240        }
241    }
242
243    /// Diagnostic storage mode. Policy code should use the capability axes.
244    #[must_use]
245    pub const fn storage_mode(self) -> StoreRuntimeStorageMode {
246        self.storage_mode
247    }
248
249    /// Allocation identity capability.
250    #[must_use]
251    pub const fn allocation_identity(self) -> StoreAllocationIdentityCapability {
252        self.allocation_identity
253    }
254
255    /// Durability capability.
256    #[must_use]
257    pub const fn durability(self) -> StoreDurability {
258        self.durability
259    }
260
261    /// Recovery capability.
262    #[must_use]
263    pub const fn recovery(self) -> StoreRecoveryCapability {
264        self.recovery
265    }
266
267    /// Commit participation capability.
268    #[must_use]
269    pub const fn commit_participation(self) -> StoreCommitParticipation {
270        self.commit_participation
271    }
272
273    /// Schema metadata persistence capability.
274    #[must_use]
275    pub const fn schema_metadata(self) -> StoreSchemaMetadataCapability {
276        self.schema_metadata
277    }
278
279    /// Relation source capability.
280    #[must_use]
281    pub const fn relation_source(self) -> StoreRelationSourceCapability {
282        self.relation_source
283    }
284
285    /// Relation target capability.
286    #[must_use]
287    pub const fn relation_target(self) -> StoreRelationTargetCapability {
288        self.relation_target
289    }
290
291    /// Live validation capability.
292    #[must_use]
293    pub const fn live_validation(self) -> StoreLiveValidationCapability {
294        self.live_validation
295    }
296}
297
298///
299/// StoreAllocationIdentity
300///
301/// Durable allocation identity for one physical stable-memory role.
302///
303
304#[derive(Clone, Copy, Debug, Eq, PartialEq)]
305pub struct StoreAllocationIdentity {
306    memory_id: u8,
307    stable_key: &'static str,
308}
309
310impl StoreAllocationIdentity {
311    /// Build one stable allocation identity descriptor.
312    #[must_use]
313    pub const fn new(memory_id: u8, stable_key: &'static str) -> Self {
314        Self {
315            memory_id,
316            stable_key,
317        }
318    }
319
320    /// Stable-memory manager ID.
321    #[must_use]
322    pub const fn memory_id(self) -> u8 {
323        self.memory_id
324    }
325
326    /// Durable stable-memory key.
327    #[must_use]
328    pub const fn stable_key(self) -> &'static str {
329        self.stable_key
330    }
331}
332
333///
334/// StoreAllocationIdentities
335///
336/// Durable allocation identities for one logical store's data, index, and
337/// schema memories.
338///
339
340#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
341pub struct StoreAllocationIdentities {
342    data: Option<StoreAllocationIdentity>,
343    index: Option<StoreAllocationIdentity>,
344    schema: Option<StoreAllocationIdentity>,
345    journal: Option<StoreAllocationIdentity>,
346}
347
348impl StoreAllocationIdentities {
349    /// Build an absent allocation identity bundle.
350    #[must_use]
351    pub const fn absent() -> Self {
352        Self {
353            data: None,
354            index: None,
355            schema: None,
356            journal: None,
357        }
358    }
359
360    /// Build one journaled cached-stable allocation identity bundle.
361    #[must_use]
362    pub const fn new_journaled(
363        data: StoreAllocationIdentity,
364        index: StoreAllocationIdentity,
365        schema: StoreAllocationIdentity,
366        journal: StoreAllocationIdentity,
367    ) -> Self {
368        Self {
369            data: Some(data),
370            index: Some(index),
371            schema: Some(schema),
372            journal: Some(journal),
373        }
374    }
375
376    /// Return data-memory allocation identity.
377    #[must_use]
378    pub const fn data(self) -> Option<StoreAllocationIdentity> {
379        self.data
380    }
381
382    /// Return index-memory allocation identity.
383    #[must_use]
384    pub const fn index(self) -> Option<StoreAllocationIdentity> {
385        self.index
386    }
387
388    /// Return schema-memory allocation identity.
389    #[must_use]
390    pub const fn schema(self) -> Option<StoreAllocationIdentity> {
391        self.schema
392    }
393
394    /// Return journal-tail allocation identity.
395    #[must_use]
396    pub const fn journal(self) -> Option<StoreAllocationIdentity> {
397        self.journal
398    }
399
400    /// Return the allocation capability represented by this triplet, or
401    /// `None` if the triplet is partially populated and therefore invalid.
402    #[must_use]
403    pub const fn allocation_identity_capability(self) -> Option<StoreAllocationIdentityCapability> {
404        match (self.data, self.index, self.schema) {
405            (Some(_), Some(_), Some(_)) => Some(StoreAllocationIdentityCapability::Present),
406            (None, None, None) if self.journal.is_none() => {
407                Some(StoreAllocationIdentityCapability::Absent)
408            }
409            _ => None,
410        }
411    }
412
413    /// Return whether this allocation shape matches the concrete storage
414    /// capability descriptor.
415    #[must_use]
416    pub const fn matches_storage_capabilities(
417        self,
418        capabilities: StoreRuntimeStorageCapabilities,
419    ) -> bool {
420        match capabilities.storage_mode() {
421            StoreRuntimeStorageMode::Heap => {
422                self.data.is_none()
423                    && self.index.is_none()
424                    && self.schema.is_none()
425                    && self.journal.is_none()
426            }
427            StoreRuntimeStorageMode::Journaled => {
428                self.data.is_some()
429                    && self.index.is_some()
430                    && self.schema.is_some()
431                    && self.journal.is_some()
432            }
433        }
434    }
435}
436
437impl StoreHandle {
438    /// Build a store handle with an explicit allocation identity decision.
439    #[must_use]
440    pub const fn new(
441        data: &'static LocalKey<RefCell<DataStore>>,
442        index: &'static LocalKey<RefCell<IndexStore>>,
443        schema: &'static LocalKey<RefCell<SchemaStore>>,
444        allocations: StoreAllocationIdentities,
445        capabilities: StoreRuntimeStorageCapabilities,
446    ) -> Self {
447        Self {
448            data,
449            index,
450            schema,
451            journal: None,
452            allocations,
453            capabilities,
454        }
455    }
456
457    /// Build a journaled store handle with an explicit journal-tail store.
458    #[must_use]
459    pub const fn new_journaled(
460        data: &'static LocalKey<RefCell<DataStore>>,
461        index: &'static LocalKey<RefCell<IndexStore>>,
462        schema: &'static LocalKey<RefCell<SchemaStore>>,
463        journal: &'static LocalKey<RefCell<JournalTailStore>>,
464        allocations: StoreAllocationIdentities,
465        capabilities: StoreRuntimeStorageCapabilities,
466    ) -> Self {
467        Self {
468            data,
469            index,
470            schema,
471            journal: Some(journal),
472            allocations,
473            capabilities,
474        }
475    }
476
477    /// Borrow the row store immutably.
478    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
479        #[cfg(feature = "diagnostics")]
480        {
481            crate::db::physical_access::measure_physical_access_operation(|| {
482                self.data.with_borrow(f)
483            })
484        }
485
486        #[cfg(not(feature = "diagnostics"))]
487        {
488            self.data.with_borrow(f)
489        }
490    }
491
492    /// Borrow the row store mutably.
493    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
494        self.data.with_borrow_mut(f)
495    }
496
497    /// Borrow the index store immutably.
498    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
499        #[cfg(feature = "diagnostics")]
500        {
501            crate::db::physical_access::measure_physical_access_operation(|| {
502                self.index.with_borrow(f)
503            })
504        }
505
506        #[cfg(not(feature = "diagnostics"))]
507        {
508            self.index.with_borrow(f)
509        }
510    }
511
512    /// Borrow the index store mutably.
513    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
514        self.index.with_borrow_mut(f)
515    }
516
517    /// Borrow the schema store immutably.
518    pub fn with_schema<R>(&self, f: impl FnOnce(&SchemaStore) -> R) -> R {
519        self.schema.with_borrow(f)
520    }
521
522    /// Borrow the schema store mutably.
523    pub fn with_schema_mut<R>(&self, f: impl FnOnce(&mut SchemaStore) -> R) -> R {
524        self.schema.with_borrow_mut(f)
525    }
526
527    /// Return the explicit lifecycle state of the bound index store.
528    #[must_use]
529    pub(in crate::db) fn index_state(&self) -> IndexState {
530        self.with_index(IndexStore::state)
531    }
532
533    /// Mark the bound index store as Building.
534    pub(in crate::db) fn mark_index_building(&self) {
535        self.with_index_mut(IndexStore::mark_building);
536    }
537
538    /// Mark the bound index store as Ready.
539    pub(in crate::db) fn mark_index_ready(&self) {
540        self.with_index_mut(IndexStore::mark_ready);
541    }
542
543    /// Return the raw row-store accessor.
544    #[must_use]
545    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
546        self.data
547    }
548
549    /// Return the raw index-store accessor.
550    #[must_use]
551    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
552        self.index
553    }
554
555    /// Return the raw schema-store accessor.
556    #[must_use]
557    pub const fn schema_store(&self) -> &'static LocalKey<RefCell<SchemaStore>> {
558        self.schema
559    }
560
561    /// Return the raw journal-tail store accessor when this store is journaled.
562    #[must_use]
563    pub const fn journal_tail_store(&self) -> Option<&'static LocalKey<RefCell<JournalTailStore>>> {
564        self.journal
565    }
566
567    /// Return the data-memory allocation identity when generated wiring
568    /// supplied it.
569    #[must_use]
570    pub const fn data_allocation(&self) -> Option<StoreAllocationIdentity> {
571        self.allocations.data()
572    }
573
574    /// Return the index-memory allocation identity when generated wiring
575    /// supplied it.
576    #[must_use]
577    pub const fn index_allocation(&self) -> Option<StoreAllocationIdentity> {
578        self.allocations.index()
579    }
580
581    /// Return the schema-memory allocation identity when generated wiring
582    /// supplied it.
583    #[must_use]
584    pub const fn schema_allocation(&self) -> Option<StoreAllocationIdentity> {
585        self.allocations.schema()
586    }
587
588    /// Return the journal-tail allocation identity when generated wiring
589    /// supplied it.
590    #[must_use]
591    pub const fn journal_allocation(&self) -> Option<StoreAllocationIdentity> {
592        self.allocations.journal()
593    }
594
595    /// Return this store's explicit runtime storage capabilities.
596    #[must_use]
597    pub const fn storage_capabilities(&self) -> StoreRuntimeStorageCapabilities {
598        self.capabilities
599    }
600}