Skip to main content

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