Skip to main content

icydb_core/db/registry/
handle.rs

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