Skip to main content

icydb_schema/node/
store.rs

1use crate::node::{
2    validate_app_memory_id, validate_memory_id_in_range, validate_memory_id_not_reserved,
3    validate_stable_key, validate_stable_key_segment,
4};
5use crate::prelude::*;
6
7///
8/// Store
9///
10/// Schema node describing the storage mode for:
11/// - primary entity data
12/// - all index data for that entity
13/// - schema metadata for that store
14///
15
16#[derive(Clone, Debug, Serialize)]
17pub struct Store {
18    def: Def,
19    ident: &'static str,
20    name: &'static str,
21    canister: &'static str,
22    storage: StoreStorage,
23}
24
25/// Storage configuration owned by one schema store declaration.
26///
27/// 0.174 admits stable, heap, and journaled cached-stable storage as distinct
28/// store storage modes. Stable-only memory ID fields must not acquire heap
29/// meaning.
30#[derive(Clone, Debug, Serialize)]
31pub enum StoreStorage {
32    /// Durable stable-memory store using one memory for data, one for indexes,
33    /// and one for accepted schema metadata.
34    Stable(StoreStableMemoryConfig),
35    /// Volatile heap store with no stable allocation identity.
36    Heap(StoreHeapConfig),
37    /// Journaled cached-stable store using canonical stable data/index/schema
38    /// memories plus a durable journal-tail memory.
39    Journaled(StoreJournaledMemoryConfig),
40}
41
42impl StoreStorage {
43    /// Borrow the stable-memory configuration.
44    ///
45    /// Future storage forms return `None`; callers that require stable memory
46    /// must fail closed or remain explicitly stable-only.
47    #[must_use]
48    pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
49        match self {
50            Self::Stable(config) => Some(config),
51            Self::Heap(_) | Self::Journaled(_) => None,
52        }
53    }
54
55    /// Borrow the journaled cached-stable configuration.
56    #[must_use]
57    pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
58        match self {
59            Self::Journaled(config) => Some(config),
60            Self::Stable(_) | Self::Heap(_) => None,
61        }
62    }
63
64    /// Return the capability descriptor derived from this storage mode.
65    #[must_use]
66    pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
67        match self {
68            Self::Stable(_) => StoreStorageCapabilities::stable(),
69            Self::Heap(_) => StoreStorageCapabilities::heap(),
70            Self::Journaled(_) => StoreStorageCapabilities::journaled(),
71        }
72    }
73}
74
75/// Diagnostic storage mode carried by a storage capability descriptor.
76///
77/// Policy code should branch on capability axes instead of this display value.
78#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
79pub enum StoreStorageMode {
80    /// Durable stable-memory storage.
81    Stable,
82    /// Volatile in-process heap storage.
83    Heap,
84    /// Journaled cached-stable durable storage.
85    Journaled,
86}
87
88/// Whether a store storage mode owns durable stable-memory allocation identity.
89#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
90pub enum AllocationIdentityCapability {
91    /// Stable allocation identity is present.
92    Present,
93    /// Stable allocation identity is absent.
94    Absent,
95}
96
97/// Store durability class.
98#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
99pub enum StoreDurability {
100    /// Store contents participate in durable storage semantics.
101    Durable,
102    /// Store contents are live-only and volatile.
103    Volatile,
104}
105
106/// Store recovery capability.
107#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
108pub enum StoreRecoveryCapability {
109    /// Store contents can be recovered through stable commit replay.
110    StableCommitReplay,
111    /// Store contents recover from canonical stable BTrees plus committed
112    /// journal tail replay.
113    StableBasePlusJournalReplay,
114    /// Store contents are not recovered.
115    None,
116}
117
118/// Store commit participation class.
119#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
120pub enum CommitParticipation {
121    /// Store mutations participate in the durable commit path.
122    Durable,
123    /// Store mutations are live-only side effects.
124    LiveOnly,
125}
126
127/// Store schema metadata persistence class.
128#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
129pub enum SchemaMetadataCapability {
130    /// Schema metadata has durable accepted-history semantics.
131    DurableAcceptedHistory,
132    /// Schema metadata is rebuilt live and is not durable history.
133    LiveRebuiltMetadata,
134    /// Schema metadata is canonical stable history plus committed journal tail.
135    CanonicalStableHistoryPlusJournalTail,
136}
137
138/// Strong relation source capability for a store.
139#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
140pub enum RelationSourceCapability {
141    /// Source rows can own durable relation integrity.
142    DurableSource,
143    /// Source rows can participate in live relation validation.
144    LiveSource,
145}
146
147/// Strong relation target capability for a store.
148#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
149pub enum RelationTargetCapability {
150    /// Target rows can be referenced by durable source rows.
151    DurableTarget,
152    /// Target rows are volatile and cannot satisfy durable source integrity.
153    VolatileTarget,
154}
155
156/// Whether the store can participate in live validation.
157#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
158pub enum LiveValidationCapability {
159    /// Live validation is supported.
160    Supported,
161}
162
163/// Storage capability descriptor derived from a store storage mode.
164///
165/// Capabilities describe storage policy. They are not allocation identity.
166/// Stable allocation identity remains `memory_id + stable_key`; heap allocation
167/// identity remains absent.
168#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
169pub struct StoreStorageCapabilities {
170    storage_mode: StoreStorageMode,
171    allocation_identity: AllocationIdentityCapability,
172    durability: StoreDurability,
173    recovery: StoreRecoveryCapability,
174    commit_participation: CommitParticipation,
175    schema_metadata: SchemaMetadataCapability,
176    relation_source: RelationSourceCapability,
177    relation_target: RelationTargetCapability,
178    live_validation: LiveValidationCapability,
179}
180
181impl StoreStorageCapabilities {
182    /// Capability descriptor for stable-memory stores.
183    #[must_use]
184    pub const fn stable() -> Self {
185        Self {
186            storage_mode: StoreStorageMode::Stable,
187            allocation_identity: AllocationIdentityCapability::Present,
188            durability: StoreDurability::Durable,
189            recovery: StoreRecoveryCapability::StableCommitReplay,
190            commit_participation: CommitParticipation::Durable,
191            schema_metadata: SchemaMetadataCapability::DurableAcceptedHistory,
192            relation_source: RelationSourceCapability::DurableSource,
193            relation_target: RelationTargetCapability::DurableTarget,
194            live_validation: LiveValidationCapability::Supported,
195        }
196    }
197
198    /// Capability descriptor for heap stores.
199    #[must_use]
200    pub const fn heap() -> Self {
201        Self {
202            storage_mode: StoreStorageMode::Heap,
203            allocation_identity: AllocationIdentityCapability::Absent,
204            durability: StoreDurability::Volatile,
205            recovery: StoreRecoveryCapability::None,
206            commit_participation: CommitParticipation::LiveOnly,
207            schema_metadata: SchemaMetadataCapability::LiveRebuiltMetadata,
208            relation_source: RelationSourceCapability::LiveSource,
209            relation_target: RelationTargetCapability::VolatileTarget,
210            live_validation: LiveValidationCapability::Supported,
211        }
212    }
213
214    /// Capability descriptor for journaled cached-stable stores.
215    #[must_use]
216    pub const fn journaled() -> Self {
217        Self {
218            storage_mode: StoreStorageMode::Journaled,
219            allocation_identity: AllocationIdentityCapability::Present,
220            durability: StoreDurability::Durable,
221            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
222            commit_participation: CommitParticipation::Durable,
223            schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
224            relation_source: RelationSourceCapability::DurableSource,
225            relation_target: RelationTargetCapability::DurableTarget,
226            live_validation: LiveValidationCapability::Supported,
227        }
228    }
229
230    /// Diagnostic storage mode. Policy code should use the capability axes.
231    #[must_use]
232    pub const fn storage_mode(self) -> StoreStorageMode {
233        self.storage_mode
234    }
235
236    /// Stable allocation identity capability.
237    #[must_use]
238    pub const fn allocation_identity(self) -> AllocationIdentityCapability {
239        self.allocation_identity
240    }
241
242    /// Durability capability.
243    #[must_use]
244    pub const fn durability(self) -> StoreDurability {
245        self.durability
246    }
247
248    /// Recovery capability.
249    #[must_use]
250    pub const fn recovery(self) -> StoreRecoveryCapability {
251        self.recovery
252    }
253
254    /// Commit participation capability.
255    #[must_use]
256    pub const fn commit_participation(self) -> CommitParticipation {
257        self.commit_participation
258    }
259
260    /// Schema metadata persistence capability.
261    #[must_use]
262    pub const fn schema_metadata(self) -> SchemaMetadataCapability {
263        self.schema_metadata
264    }
265
266    /// Relation source capability.
267    #[must_use]
268    pub const fn relation_source(self) -> RelationSourceCapability {
269        self.relation_source
270    }
271
272    /// Relation target capability.
273    #[must_use]
274    pub const fn relation_target(self) -> RelationTargetCapability {
275        self.relation_target
276    }
277
278    /// Live validation capability.
279    #[must_use]
280    pub const fn live_validation(self) -> LiveValidationCapability {
281        self.live_validation
282    }
283
284    /// Return whether stable allocation identity is present.
285    #[must_use]
286    pub const fn has_allocation_identity(self) -> bool {
287        matches!(
288            self.allocation_identity,
289            AllocationIdentityCapability::Present
290        )
291    }
292
293    /// Return whether mutations participate in durable commit.
294    #[must_use]
295    pub const fn participates_in_durable_commit(self) -> bool {
296        matches!(self.commit_participation, CommitParticipation::Durable)
297    }
298
299    /// Return whether the store is volatile.
300    #[must_use]
301    pub const fn is_volatile(self) -> bool {
302        matches!(self.durability, StoreDurability::Volatile)
303    }
304}
305
306/// Heap storage configuration for one volatile store.
307#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
308pub struct StoreHeapConfig;
309
310impl StoreHeapConfig {
311    /// Build an empty heap storage configuration.
312    #[must_use]
313    pub const fn new() -> Self {
314        Self
315    }
316}
317
318/// Stable-memory IDs for the three durable roles owned by one store.
319#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
320pub struct StoreStableMemoryConfig {
321    data: u8,
322    index: u8,
323    schema: u8,
324}
325
326/// Stable-memory IDs for the four durable roles owned by one journaled
327/// cached-stable store.
328#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
329pub struct StoreJournaledMemoryConfig {
330    data: u8,
331    index: u8,
332    schema: u8,
333    journal: u8,
334}
335
336impl StoreJournaledMemoryConfig {
337    /// Build a journaled memory configuration from canonical data, index,
338    /// schema, and journal-tail memory IDs.
339    #[must_use]
340    pub const fn new(
341        data_memory_id: u8,
342        index_memory_id: u8,
343        schema_memory_id: u8,
344        journal_memory_id: u8,
345    ) -> Self {
346        Self {
347            data: data_memory_id,
348            index: index_memory_id,
349            schema: schema_memory_id,
350            journal: journal_memory_id,
351        }
352    }
353
354    /// Canonical data-store stable memory ID.
355    #[must_use]
356    pub const fn data_memory_id(self) -> u8 {
357        self.data
358    }
359
360    /// Canonical index-store stable memory ID.
361    #[must_use]
362    pub const fn index_memory_id(self) -> u8 {
363        self.index
364    }
365
366    /// Canonical schema-store stable memory ID.
367    #[must_use]
368    pub const fn schema_memory_id(self) -> u8 {
369        self.schema
370    }
371
372    /// Durable journal-tail stable memory ID.
373    #[must_use]
374    pub const fn journal_memory_id(self) -> u8 {
375        self.journal
376    }
377}
378
379impl StoreStableMemoryConfig {
380    /// Build a stable-memory configuration from data, index, and schema memory
381    /// IDs.
382    #[must_use]
383    pub const fn new(data_memory_id: u8, index_memory_id: u8, schema_memory_id: u8) -> Self {
384        Self {
385            data: data_memory_id,
386            index: index_memory_id,
387            schema: schema_memory_id,
388        }
389    }
390
391    /// Data-store stable memory ID.
392    #[must_use]
393    pub const fn data_memory_id(self) -> u8 {
394        self.data
395    }
396
397    /// Index-store stable memory ID.
398    #[must_use]
399    pub const fn index_memory_id(self) -> u8 {
400        self.index
401    }
402
403    /// Schema-store stable memory ID.
404    #[must_use]
405    pub const fn schema_memory_id(self) -> u8 {
406        self.schema
407    }
408}
409
410impl Store {
411    /// Build a stable-memory-backed store declaration.
412    ///
413    /// This is the durable store constructor.
414    #[must_use]
415    pub const fn new_stable(
416        def: Def,
417        ident: &'static str,
418        store_name: &'static str,
419        canister: &'static str,
420        stable: StoreStableMemoryConfig,
421    ) -> Self {
422        Self {
423            def,
424            ident,
425            name: store_name,
426            canister,
427            storage: StoreStorage::Stable(stable),
428        }
429    }
430
431    /// Build a heap-backed volatile store declaration.
432    #[must_use]
433    pub const fn new_heap(
434        def: Def,
435        ident: &'static str,
436        store_name: &'static str,
437        canister: &'static str,
438        heap: StoreHeapConfig,
439    ) -> Self {
440        Self {
441            def,
442            ident,
443            name: store_name,
444            canister,
445            storage: StoreStorage::Heap(heap),
446        }
447    }
448
449    /// Build a journaled cached-stable store declaration.
450    #[must_use]
451    pub const fn new_journaled(
452        def: Def,
453        ident: &'static str,
454        store_name: &'static str,
455        canister: &'static str,
456        journaled: StoreJournaledMemoryConfig,
457    ) -> Self {
458        Self {
459            def,
460            ident,
461            name: store_name,
462            canister,
463            storage: StoreStorage::Journaled(journaled),
464        }
465    }
466
467    #[must_use]
468    pub const fn def(&self) -> &Def {
469        &self.def
470    }
471
472    #[must_use]
473    pub const fn ident(&self) -> &'static str {
474        self.ident
475    }
476
477    #[must_use]
478    pub const fn store_name(&self) -> &'static str {
479        self.name
480    }
481
482    #[must_use]
483    pub const fn canister(&self) -> &'static str {
484        self.canister
485    }
486
487    /// Borrow this store's storage configuration.
488    #[must_use]
489    pub const fn storage(&self) -> &StoreStorage {
490        &self.storage
491    }
492
493    /// Return whether this store is stable-memory-backed.
494    #[must_use]
495    pub const fn is_stable_storage(&self) -> bool {
496        matches!(self.storage, StoreStorage::Stable(_))
497    }
498
499    /// Return whether this store is heap-backed and volatile.
500    #[must_use]
501    pub const fn is_heap_storage(&self) -> bool {
502        matches!(self.storage, StoreStorage::Heap(_))
503    }
504
505    /// Return whether this store is journaled cached-stable.
506    #[must_use]
507    pub const fn is_journaled_storage(&self) -> bool {
508        matches!(self.storage, StoreStorage::Journaled(_))
509    }
510
511    /// Borrow stable-memory IDs when this store uses stable storage.
512    #[must_use]
513    pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
514        self.storage.stable_memory_config()
515    }
516
517    /// Borrow journaled cached-stable memory IDs when this store uses
518    /// journaled storage.
519    #[must_use]
520    pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
521        self.storage.journaled_memory_config()
522    }
523
524    /// Return the capability descriptor derived from this store's storage mode.
525    #[must_use]
526    pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
527        self.storage.storage_capabilities()
528    }
529
530    #[must_use]
531    pub const fn stable_data_memory_id(&self) -> u8 {
532        match self.storage {
533            StoreStorage::Stable(config) => config.data_memory_id(),
534            StoreStorage::Journaled(config) => config.data_memory_id(),
535            StoreStorage::Heap(_) => panic!("heap stores do not have a stable data memory id"),
536        }
537    }
538
539    #[must_use]
540    pub const fn stable_index_memory_id(&self) -> u8 {
541        match self.storage {
542            StoreStorage::Stable(config) => config.index_memory_id(),
543            StoreStorage::Journaled(config) => config.index_memory_id(),
544            StoreStorage::Heap(_) => panic!("heap stores do not have a stable index memory id"),
545        }
546    }
547
548    #[must_use]
549    pub const fn stable_schema_memory_id(&self) -> u8 {
550        match self.storage {
551            StoreStorage::Stable(config) => config.schema_memory_id(),
552            StoreStorage::Journaled(config) => config.schema_memory_id(),
553            StoreStorage::Heap(_) => panic!("heap stores do not have a stable schema memory id"),
554        }
555    }
556
557    #[must_use]
558    pub const fn journal_memory_id(&self) -> u8 {
559        match self.storage {
560            StoreStorage::Journaled(config) => config.journal_memory_id(),
561            StoreStorage::Stable(_) => {
562                panic!("stable stores do not have a journal memory id")
563            }
564            StoreStorage::Heap(_) => panic!("heap stores do not have a journal memory id"),
565        }
566    }
567
568    #[must_use]
569    pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
570        self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
571    }
572
573    /// Build the data-memory allocation descriptor with accepted row-layout
574    /// schema metadata attached for diagnostics.
575    #[must_use]
576    pub fn stable_data_allocation_with_schema_metadata(
577        &self,
578        memory_namespace: &str,
579        schema_metadata: StableMemoryAllocationMetadata,
580    ) -> StableMemoryAllocation {
581        self.stable_allocation_with_schema_metadata(
582            memory_namespace,
583            StoreMemoryRole::Data,
584            schema_metadata,
585        )
586    }
587
588    #[must_use]
589    pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
590        self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
591    }
592
593    /// Build the index-memory allocation descriptor with accepted index-catalog
594    /// schema metadata attached for diagnostics.
595    #[must_use]
596    pub fn stable_index_allocation_with_schema_metadata(
597        &self,
598        memory_namespace: &str,
599        schema_metadata: StableMemoryAllocationMetadata,
600    ) -> StableMemoryAllocation {
601        self.stable_allocation_with_schema_metadata(
602            memory_namespace,
603            StoreMemoryRole::Index,
604            schema_metadata,
605        )
606    }
607
608    #[must_use]
609    pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
610        self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
611    }
612
613    /// Build the journal-tail allocation descriptor for journaled stores.
614    #[must_use]
615    pub fn journal_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
616        StableMemoryAllocation::without_schema_metadata(
617            self.journal_memory_id(),
618            stable_memory_key(memory_namespace, self.store_name(), "journal"),
619        )
620    }
621
622    /// Build the schema-memory allocation descriptor with accepted catalog
623    /// schema metadata attached for diagnostics.
624    #[must_use]
625    pub fn stable_schema_allocation_with_schema_metadata(
626        &self,
627        memory_namespace: &str,
628        schema_metadata: StableMemoryAllocationMetadata,
629    ) -> StableMemoryAllocation {
630        self.stable_allocation_with_schema_metadata(
631            memory_namespace,
632            StoreMemoryRole::Schema,
633            schema_metadata,
634        )
635    }
636
637    #[must_use]
638    pub fn stable_allocation(
639        &self,
640        memory_namespace: &str,
641        role: StoreMemoryRole,
642    ) -> StableMemoryAllocation {
643        let memory_id = match role {
644            StoreMemoryRole::Data => self.stable_data_memory_id(),
645            StoreMemoryRole::Index => self.stable_index_memory_id(),
646            StoreMemoryRole::Schema => self.stable_schema_memory_id(),
647        };
648
649        StableMemoryAllocation::without_schema_metadata(
650            memory_id,
651            stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
652        )
653    }
654
655    fn stable_allocation_with_schema_metadata(
656        &self,
657        memory_namespace: &str,
658        role: StoreMemoryRole,
659        schema_metadata: StableMemoryAllocationMetadata,
660    ) -> StableMemoryAllocation {
661        let memory_id = match role {
662            StoreMemoryRole::Data => self.stable_data_memory_id(),
663            StoreMemoryRole::Index => self.stable_index_memory_id(),
664            StoreMemoryRole::Schema => self.stable_schema_memory_id(),
665        };
666
667        StableMemoryAllocation::with_schema_metadata(
668            memory_id,
669            stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
670            schema_metadata,
671        )
672    }
673}
674
675#[derive(Clone, Copy, Debug, Eq, PartialEq)]
676pub enum StoreMemoryRole {
677    Data,
678    Index,
679    Schema,
680}
681
682impl StoreMemoryRole {
683    #[must_use]
684    pub const fn as_str(self) -> &'static str {
685        match self {
686            Self::Data => "data",
687            Self::Index => "index",
688            Self::Schema => "schema",
689        }
690    }
691}
692
693/// Diagnostic schema metadata associated with a stable-memory allocation.
694///
695/// This metadata does not participate in durable allocation identity. The
696/// durable identity remains `memory_id + stable_key`.
697#[derive(Clone, Debug, Eq, PartialEq)]
698pub struct StableMemoryAllocationMetadata {
699    schema_version: Option<u32>,
700    schema_fingerprint: Option<String>,
701}
702
703impl StableMemoryAllocationMetadata {
704    const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
705        Self {
706            schema_version,
707            schema_fingerprint,
708        }
709    }
710
711    /// Build allocation metadata from an accepted schema/catalog authority.
712    #[must_use]
713    pub const fn from_accepted_schema_contract(
714        schema_version: u32,
715        schema_fingerprint: String,
716    ) -> Self {
717        Self::new(Some(schema_version), Some(schema_fingerprint))
718    }
719
720    /// Build absent allocation metadata for allocations with no accepted
721    /// schema/catalog authority.
722    #[must_use]
723    pub const fn absent() -> Self {
724        Self::new(None, None)
725    }
726
727    /// Accepted schema/catalog version, when known.
728    #[must_use]
729    pub const fn schema_version(&self) -> Option<u32> {
730        self.schema_version
731    }
732
733    /// Accepted schema/catalog fingerprint, when known.
734    #[must_use]
735    pub const fn schema_fingerprint(&self) -> Option<&str> {
736        match &self.schema_fingerprint {
737            Some(value) => Some(value.as_str()),
738            None => None,
739        }
740    }
741}
742
743/// Stable-memory allocation descriptor.
744///
745/// `memory_id + stable_key` is the durable allocation identity.
746/// `schema_version + schema_fingerprint` is diagnostic metadata only.
747#[derive(Clone, Debug, Eq, PartialEq)]
748pub struct StableMemoryAllocation {
749    memory_id: u8,
750    stable_key: String,
751    schema_metadata: StableMemoryAllocationMetadata,
752}
753
754impl StableMemoryAllocation {
755    /// Build an allocation descriptor without schema metadata.
756    #[must_use]
757    pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
758        Self::with_schema_metadata(
759            memory_id,
760            stable_key,
761            StableMemoryAllocationMetadata::absent(),
762        )
763    }
764
765    /// Build an allocation descriptor with diagnostic schema metadata.
766    ///
767    /// The metadata must come from accepted schema/catalog authority. Generated
768    /// model fallback metadata is not an allocation metadata authority.
769    #[must_use]
770    pub const fn with_schema_metadata(
771        memory_id: u8,
772        stable_key: String,
773        schema_metadata: StableMemoryAllocationMetadata,
774    ) -> Self {
775        Self {
776            memory_id,
777            stable_key,
778            schema_metadata,
779        }
780    }
781
782    /// Stable-memory manager ID.
783    #[must_use]
784    pub const fn memory_id(&self) -> u8 {
785        self.memory_id
786    }
787
788    /// Durable stable-memory key.
789    #[must_use]
790    pub const fn stable_key(&self) -> &str {
791        self.stable_key.as_str()
792    }
793
794    /// Diagnostic schema/catalog metadata.
795    #[must_use]
796    pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
797        &self.schema_metadata
798    }
799
800    /// Accepted schema/catalog version, when known.
801    #[must_use]
802    pub const fn schema_version(&self) -> Option<u32> {
803        self.schema_metadata.schema_version()
804    }
805
806    /// Accepted schema/catalog fingerprint, when known.
807    #[must_use]
808    pub const fn schema_fingerprint(&self) -> Option<&str> {
809        self.schema_metadata.schema_fingerprint()
810    }
811
812    /// Compare durable allocation identity only.
813    ///
814    /// Schema metadata is intentionally ignored because metadata changes are
815    /// diagnostics, not memory replacement.
816    #[must_use]
817    pub fn same_identity_as(&self, other: &Self) -> bool {
818        self.memory_id == other.memory_id && self.stable_key == other.stable_key
819    }
820}
821
822#[must_use]
823pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
824    format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
825}
826
827impl MacroNode for Store {
828    fn as_any(&self) -> &dyn std::any::Any {
829        self
830    }
831}
832
833impl ValidateNode for Store {
834    fn validate(&self) -> Result<(), ErrorTree> {
835        let mut errs = ErrorTree::new();
836        let schema = schema_read();
837
838        match schema.cast_node::<Canister>(self.canister()) {
839            Ok(canister) => {
840                validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
841                match self.storage() {
842                    StoreStorage::Stable(config) => {
843                        validate_stable_memory_config(&mut errs, self, *config, canister);
844                    }
845                    StoreStorage::Heap(_) => {}
846                    StoreStorage::Journaled(config) => {
847                        validate_journaled_memory_config(&mut errs, self, *config, canister);
848                    }
849                }
850            }
851            Err(e) => errs.add(e),
852        }
853
854        errs.result()
855    }
856}
857
858fn validate_journaled_memory_config(
859    errs: &mut ErrorTree,
860    store: &Store,
861    config: StoreJournaledMemoryConfig,
862    canister: &Canister,
863) {
864    validate_stable_memory_role(
865        errs,
866        "data_memory_id",
867        "data stable key",
868        config.data_memory_id(),
869        store
870            .stable_data_allocation(canister.memory_namespace())
871            .stable_key(),
872        canister,
873    );
874    validate_stable_memory_role(
875        errs,
876        "index_memory_id",
877        "index stable key",
878        config.index_memory_id(),
879        store
880            .stable_index_allocation(canister.memory_namespace())
881            .stable_key(),
882        canister,
883    );
884    validate_stable_memory_role(
885        errs,
886        "schema_memory_id",
887        "schema stable key",
888        config.schema_memory_id(),
889        store
890            .stable_schema_allocation(canister.memory_namespace())
891            .stable_key(),
892        canister,
893    );
894    validate_stable_memory_role(
895        errs,
896        "journal_memory_id",
897        "journal stable key",
898        config.journal_memory_id(),
899        store
900            .journal_allocation(canister.memory_namespace())
901            .stable_key(),
902        canister,
903    );
904
905    validate_distinct_journaled_memory_ids(errs, config);
906}
907
908fn validate_distinct_journaled_memory_ids(
909    errs: &mut ErrorTree,
910    config: StoreJournaledMemoryConfig,
911) {
912    let roles = [
913        ("data_memory_id", config.data_memory_id()),
914        ("index_memory_id", config.index_memory_id()),
915        ("schema_memory_id", config.schema_memory_id()),
916        ("journal_memory_id", config.journal_memory_id()),
917    ];
918
919    for (idx, (left_label, left_id)) in roles.iter().enumerate() {
920        for (right_label, right_id) in roles.iter().skip(idx + 1) {
921            if left_id == right_id {
922                err!(
923                    errs,
924                    "{} and {} must differ (both are {})",
925                    left_label,
926                    right_label,
927                    left_id,
928                );
929            }
930        }
931    }
932}
933
934fn validate_stable_memory_config(
935    errs: &mut ErrorTree,
936    store: &Store,
937    config: StoreStableMemoryConfig,
938    canister: &Canister,
939) {
940    validate_stable_memory_role(
941        errs,
942        "data_memory_id",
943        "data stable key",
944        config.data_memory_id(),
945        store
946            .stable_data_allocation(canister.memory_namespace())
947            .stable_key(),
948        canister,
949    );
950    validate_stable_memory_role(
951        errs,
952        "index_memory_id",
953        "index stable key",
954        config.index_memory_id(),
955        store
956            .stable_index_allocation(canister.memory_namespace())
957            .stable_key(),
958        canister,
959    );
960    validate_stable_memory_role(
961        errs,
962        "schema_memory_id",
963        "schema stable key",
964        config.schema_memory_id(),
965        store
966            .stable_schema_allocation(canister.memory_namespace())
967            .stable_key(),
968        canister,
969    );
970
971    if config.data_memory_id() == config.index_memory_id() {
972        err!(
973            errs,
974            "data_memory_id and index_memory_id must differ (both are {})",
975            config.data_memory_id(),
976        );
977    }
978    if config.data_memory_id() == config.schema_memory_id() {
979        err!(
980            errs,
981            "data_memory_id and schema_memory_id must differ (both are {})",
982            config.data_memory_id(),
983        );
984    }
985    if config.index_memory_id() == config.schema_memory_id() {
986        err!(
987            errs,
988            "index_memory_id and schema_memory_id must differ (both are {})",
989            config.index_memory_id(),
990        );
991    }
992}
993
994fn validate_stable_memory_role(
995    errs: &mut ErrorTree,
996    memory_label: &str,
997    stable_key_label: &str,
998    memory_id: u8,
999    stable_key: &str,
1000    canister: &Canister,
1001) {
1002    validate_memory_id_in_range(
1003        errs,
1004        memory_label,
1005        memory_id,
1006        canister.memory_min(),
1007        canister.memory_max(),
1008    );
1009    validate_app_memory_id(errs, memory_label, memory_id);
1010    validate_memory_id_not_reserved(errs, memory_label, memory_id);
1011    validate_stable_key(errs, stable_key_label, stable_key);
1012}
1013
1014impl VisitableNode for Store {
1015    fn route_key(&self) -> String {
1016        self.def().path()
1017    }
1018
1019    fn drive<V: Visitor>(&self, v: &mut V) {
1020        self.def().accept(v);
1021    }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026    use crate::{
1027        build::schema_write,
1028        node::{Canister, SchemaNode},
1029    };
1030
1031    use super::*;
1032
1033    fn insert_canister(path_module: &'static str, ident: &'static str) {
1034        schema_write().insert_node(SchemaNode::Canister(Canister::new(
1035            Def::new(path_module, ident),
1036            "test_db",
1037            100,
1038            254,
1039            254,
1040        )));
1041    }
1042
1043    #[test]
1044    fn store_stable_keys_use_durable_icydb_shape() {
1045        let store = Store::new_stable(
1046            Def::new("demo::rpg", "CharacterStore"),
1047            "CHARACTER_STORE",
1048            "characters",
1049            "demo::rpg::Canister",
1050            StoreStableMemoryConfig::new(110, 111, 112),
1051        );
1052
1053        assert_eq!(
1054            store.stable_data_allocation("demo_rpg").stable_key(),
1055            "icydb.demo_rpg.characters.data.v1",
1056        );
1057        assert_eq!(
1058            store.stable_index_allocation("demo_rpg").stable_key(),
1059            "icydb.demo_rpg.characters.index.v1",
1060        );
1061        assert_eq!(
1062            store.stable_schema_allocation("demo_rpg").stable_key(),
1063            "icydb.demo_rpg.characters.schema.v1",
1064        );
1065    }
1066
1067    #[test]
1068    fn store_allocations_default_to_absent_schema_metadata() {
1069        let store = Store::new_stable(
1070            Def::new("demo::rpg", "CharacterStore"),
1071            "CHARACTER_STORE",
1072            "characters",
1073            "demo::rpg::Canister",
1074            StoreStableMemoryConfig::new(110, 111, 112),
1075        );
1076
1077        for allocation in [
1078            store.stable_data_allocation("demo_rpg"),
1079            store.stable_index_allocation("demo_rpg"),
1080            store.stable_schema_allocation("demo_rpg"),
1081        ] {
1082            assert_eq!(allocation.schema_version(), None);
1083            assert_eq!(allocation.schema_fingerprint(), None);
1084            assert_eq!(
1085                allocation.schema_metadata(),
1086                &StableMemoryAllocationMetadata::absent()
1087            );
1088        }
1089    }
1090
1091    #[test]
1092    fn allocation_metadata_is_role_specific_and_diagnostic_only() {
1093        let store = Store::new_stable(
1094            Def::new("demo::rpg", "CharacterStore"),
1095            "CHARACTER_STORE",
1096            "characters",
1097            "demo::rpg::Canister",
1098            StoreStableMemoryConfig::new(110, 111, 112),
1099        );
1100        let data = store.stable_data_allocation_with_schema_metadata(
1101            "demo_rpg",
1102            StableMemoryAllocationMetadata::from_accepted_schema_contract(
1103                7,
1104                "data-row-layout".to_string(),
1105            ),
1106        );
1107        let index = store.stable_index_allocation_with_schema_metadata(
1108            "demo_rpg",
1109            StableMemoryAllocationMetadata::from_accepted_schema_contract(
1110                8,
1111                "index-catalog".to_string(),
1112            ),
1113        );
1114        let schema = store.stable_schema_allocation_with_schema_metadata(
1115            "demo_rpg",
1116            StableMemoryAllocationMetadata::from_accepted_schema_contract(
1117                10,
1118                "schema-catalog".to_string(),
1119            ),
1120        );
1121        let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
1122            "demo_rpg",
1123            StableMemoryAllocationMetadata::from_accepted_schema_contract(
1124                9,
1125                "data-row-layout-v2".to_string(),
1126            ),
1127        );
1128
1129        assert_eq!(data.schema_version(), Some(7));
1130        assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
1131        assert_eq!(index.schema_version(), Some(8));
1132        assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
1133        assert_eq!(schema.schema_version(), Some(10));
1134        assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
1135        assert!(data.same_identity_as(&data_after_reconcile));
1136        assert!(!data.same_identity_as(&index));
1137        assert!(!data.same_identity_as(&schema));
1138    }
1139
1140    #[test]
1141    fn store_owns_explicit_stable_storage_config() {
1142        let store = Store::new_stable(
1143            Def::new("demo::rpg", "CharacterStore"),
1144            "CHARACTER_STORE",
1145            "characters",
1146            "demo::rpg::Canister",
1147            StoreStableMemoryConfig::new(110, 111, 112),
1148        );
1149
1150        assert!(store.is_stable_storage());
1151        assert!(store.storage().stable_memory_config().is_some());
1152        let stable = store
1153            .stable_memory_config()
1154            .expect("0.167 model stores stable config explicitly");
1155
1156        assert_eq!(stable.data_memory_id(), 110);
1157        assert_eq!(stable.index_memory_id(), 111);
1158        assert_eq!(stable.schema_memory_id(), 112);
1159        assert_eq!(store.stable_data_memory_id(), 110);
1160        assert_eq!(store.stable_index_memory_id(), 111);
1161        assert_eq!(store.stable_schema_memory_id(), 112);
1162    }
1163
1164    #[test]
1165    fn stable_store_storage_capabilities_describe_durable_contract() {
1166        let store = Store::new_stable(
1167            Def::new("demo::rpg", "CharacterStore"),
1168            "CHARACTER_STORE",
1169            "characters",
1170            "demo::rpg::Canister",
1171            StoreStableMemoryConfig::new(110, 111, 112),
1172        );
1173        let capabilities = store.storage_capabilities();
1174
1175        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Stable);
1176        assert_eq!(
1177            capabilities.allocation_identity(),
1178            AllocationIdentityCapability::Present,
1179        );
1180        assert_eq!(capabilities.durability(), StoreDurability::Durable);
1181        assert_eq!(
1182            capabilities.recovery(),
1183            StoreRecoveryCapability::StableCommitReplay,
1184        );
1185        assert_eq!(
1186            capabilities.commit_participation(),
1187            CommitParticipation::Durable,
1188        );
1189        assert_eq!(
1190            capabilities.schema_metadata(),
1191            SchemaMetadataCapability::DurableAcceptedHistory,
1192        );
1193        assert_eq!(
1194            capabilities.relation_source(),
1195            RelationSourceCapability::DurableSource,
1196        );
1197        assert_eq!(
1198            capabilities.relation_target(),
1199            RelationTargetCapability::DurableTarget,
1200        );
1201        assert_eq!(
1202            capabilities.live_validation(),
1203            LiveValidationCapability::Supported,
1204        );
1205        assert!(capabilities.has_allocation_identity());
1206        assert!(capabilities.participates_in_durable_commit());
1207        assert!(!capabilities.is_volatile());
1208    }
1209
1210    #[test]
1211    fn store_owns_explicit_heap_storage_config() {
1212        insert_canister("store_heap_config", "Canister");
1213        let store = Store::new_heap(
1214            Def::new("store_heap_config", "Store"),
1215            "STORE",
1216            "heap_store",
1217            "store_heap_config::Canister",
1218            StoreHeapConfig::new(),
1219        );
1220
1221        assert!(store.is_heap_storage());
1222        assert!(!store.is_stable_storage());
1223        assert!(store.stable_memory_config().is_none());
1224        assert!(store.validate().is_ok());
1225    }
1226
1227    #[test]
1228    fn heap_store_storage_capabilities_describe_volatile_contract() {
1229        let store = Store::new_heap(
1230            Def::new("store_heap_capabilities", "Store"),
1231            "STORE",
1232            "heap_store",
1233            "store_heap_capabilities::Canister",
1234            StoreHeapConfig::new(),
1235        );
1236        let capabilities = store.storage_capabilities();
1237
1238        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
1239        assert_eq!(
1240            capabilities.allocation_identity(),
1241            AllocationIdentityCapability::Absent,
1242        );
1243        assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1244        assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1245        assert_eq!(
1246            capabilities.commit_participation(),
1247            CommitParticipation::LiveOnly,
1248        );
1249        assert_eq!(
1250            capabilities.schema_metadata(),
1251            SchemaMetadataCapability::LiveRebuiltMetadata,
1252        );
1253        assert_eq!(
1254            capabilities.relation_source(),
1255            RelationSourceCapability::LiveSource,
1256        );
1257        assert_eq!(
1258            capabilities.relation_target(),
1259            RelationTargetCapability::VolatileTarget,
1260        );
1261        assert_eq!(
1262            capabilities.live_validation(),
1263            LiveValidationCapability::Supported,
1264        );
1265        assert!(!capabilities.has_allocation_identity());
1266        assert!(!capabilities.participates_in_durable_commit());
1267        assert!(capabilities.is_volatile());
1268    }
1269
1270    #[test]
1271    fn store_owns_explicit_journaled_storage_config() {
1272        insert_canister("store_journaled_config", "Canister");
1273        let store = Store::new_journaled(
1274            Def::new("store_journaled_config", "Store"),
1275            "STORE",
1276            "journaled_store",
1277            "store_journaled_config::Canister",
1278            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1279        );
1280
1281        assert!(store.is_journaled_storage());
1282        assert!(!store.is_stable_storage());
1283        assert!(!store.is_heap_storage());
1284        let journaled = store
1285            .journaled_memory_config()
1286            .expect("journaled model stores four-role config explicitly");
1287
1288        assert_eq!(journaled.data_memory_id(), 110);
1289        assert_eq!(journaled.index_memory_id(), 111);
1290        assert_eq!(journaled.schema_memory_id(), 112);
1291        assert_eq!(journaled.journal_memory_id(), 113);
1292        assert_eq!(store.stable_data_memory_id(), 110);
1293        assert_eq!(store.stable_index_memory_id(), 111);
1294        assert_eq!(store.stable_schema_memory_id(), 112);
1295        assert_eq!(store.journal_memory_id(), 113);
1296        assert!(store.validate().is_ok());
1297    }
1298
1299    #[test]
1300    fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
1301        let store = Store::new_journaled(
1302            Def::new("store_journaled_capabilities", "Store"),
1303            "STORE",
1304            "journaled_store",
1305            "store_journaled_capabilities::Canister",
1306            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1307        );
1308        let capabilities = store.storage_capabilities();
1309
1310        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1311        assert_eq!(
1312            capabilities.allocation_identity(),
1313            AllocationIdentityCapability::Present,
1314        );
1315        assert_eq!(capabilities.durability(), StoreDurability::Durable);
1316        assert_eq!(
1317            capabilities.recovery(),
1318            StoreRecoveryCapability::StableBasePlusJournalReplay,
1319        );
1320        assert_eq!(
1321            capabilities.commit_participation(),
1322            CommitParticipation::Durable,
1323        );
1324        assert_eq!(
1325            capabilities.schema_metadata(),
1326            SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1327        );
1328        assert_eq!(
1329            capabilities.relation_source(),
1330            RelationSourceCapability::DurableSource,
1331        );
1332        assert_eq!(
1333            capabilities.relation_target(),
1334            RelationTargetCapability::DurableTarget,
1335        );
1336        assert_eq!(
1337            capabilities.live_validation(),
1338            LiveValidationCapability::Supported,
1339        );
1340        assert!(capabilities.has_allocation_identity());
1341        assert!(capabilities.participates_in_durable_commit());
1342        assert!(!capabilities.is_volatile());
1343    }
1344
1345    #[test]
1346    fn journaled_store_allocations_use_role_named_stable_keys() {
1347        let store = Store::new_journaled(
1348            Def::new("demo::rpg", "CharacterStore"),
1349            "CHARACTER_STORE",
1350            "characters",
1351            "demo::rpg::Canister",
1352            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1353        );
1354
1355        assert_eq!(
1356            store.stable_data_allocation("demo_rpg").stable_key(),
1357            "icydb.demo_rpg.characters.data.v1",
1358        );
1359        assert_eq!(
1360            store.stable_index_allocation("demo_rpg").stable_key(),
1361            "icydb.demo_rpg.characters.index.v1",
1362        );
1363        assert_eq!(
1364            store.stable_schema_allocation("demo_rpg").stable_key(),
1365            "icydb.demo_rpg.characters.schema.v1",
1366        );
1367        assert_eq!(
1368            store.journal_allocation("demo_rpg").stable_key(),
1369            "icydb.demo_rpg.characters.journal.v1",
1370        );
1371    }
1372
1373    #[test]
1374    fn storage_capabilities_are_not_allocation_identity() {
1375        let store_a = Store::new_stable(
1376            Def::new("demo::rpg", "CharacterStore"),
1377            "CHARACTER_STORE",
1378            "characters",
1379            "demo::rpg::Canister",
1380            StoreStableMemoryConfig::new(110, 111, 112),
1381        );
1382        let store_b = Store::new_stable(
1383            Def::new("demo::rpg", "InventoryStore"),
1384            "INVENTORY_STORE",
1385            "inventory",
1386            "demo::rpg::Canister",
1387            StoreStableMemoryConfig::new(120, 121, 122),
1388        );
1389
1390        assert_eq!(
1391            store_a.storage_capabilities(),
1392            store_b.storage_capabilities()
1393        );
1394        assert_ne!(
1395            store_a.stable_data_allocation("demo_rpg"),
1396            store_b.stable_data_allocation("demo_rpg"),
1397            "stable allocation identity must remain separate from capabilities",
1398        );
1399    }
1400
1401    #[test]
1402    fn capability_consumers_use_axes_not_storage_mode() {
1403        const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1404            match capabilities.commit_participation() {
1405                CommitParticipation::Durable => "durable",
1406                CommitParticipation::LiveOnly => "live-only",
1407            }
1408        }
1409
1410        let future_durable_heap_mode = StoreStorageCapabilities {
1411            storage_mode: StoreStorageMode::Heap,
1412            allocation_identity: AllocationIdentityCapability::Present,
1413            durability: StoreDurability::Durable,
1414            recovery: StoreRecoveryCapability::StableCommitReplay,
1415            commit_participation: CommitParticipation::Durable,
1416            schema_metadata: SchemaMetadataCapability::DurableAcceptedHistory,
1417            relation_source: RelationSourceCapability::DurableSource,
1418            relation_target: RelationTargetCapability::DurableTarget,
1419            live_validation: LiveValidationCapability::Supported,
1420        };
1421
1422        assert_eq!(commit_label(future_durable_heap_mode), "durable");
1423        assert!(future_durable_heap_mode.participates_in_durable_commit());
1424        assert_eq!(
1425            future_durable_heap_mode.storage_mode(),
1426            StoreStorageMode::Heap,
1427            "the diagnostic storage mode must not drive commit policy",
1428        );
1429    }
1430
1431    #[test]
1432    fn store_stable_storage_config_rejects_duplicate_role_memory_ids() {
1433        insert_canister("store_duplicate_role_memory_ids", "Canister");
1434        let store = Store::new_stable(
1435            Def::new("store_duplicate_role_memory_ids", "Store"),
1436            "STORE",
1437            "duplicate_role_memory_ids",
1438            "store_duplicate_role_memory_ids::Canister",
1439            StoreStableMemoryConfig::new(110, 110, 112),
1440        );
1441
1442        let err = store
1443            .validate()
1444            .expect_err("duplicate store role memory IDs must fail validation");
1445        let rendered = err.to_string();
1446
1447        assert!(
1448            rendered.contains("data_memory_id and index_memory_id must differ"),
1449            "expected duplicate role memory-id error, got: {rendered}"
1450        );
1451    }
1452
1453    #[test]
1454    fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1455        insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1456        let store = Store::new_journaled(
1457            Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1458            "STORE",
1459            "duplicate_journaled_role_memory_ids",
1460            "store_duplicate_journaled_role_memory_ids::Canister",
1461            StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1462        );
1463
1464        let err = store
1465            .validate()
1466            .expect_err("duplicate journaled role memory IDs must fail validation");
1467        let rendered = err.to_string();
1468
1469        assert!(
1470            rendered.contains("schema_memory_id and journal_memory_id must differ"),
1471            "expected duplicate journaled role memory-id error, got: {rendered}"
1472        );
1473    }
1474}