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