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