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
771        {
772            let schema = schema_read();
773
774            match schema.cast_node::<Canister>(self.canister()) {
775                Ok(canister) => {
776                    validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
777                    match self.storage() {
778                        StoreStorage::Heap(_) => {}
779                        StoreStorage::Journaled(config) => {
780                            validate_journaled_memory_config(&mut errs, self, *config, canister);
781                        }
782                    }
783                }
784                Err(e) => errs.add(e),
785            }
786        }
787
788        errs.result()
789    }
790}
791
792fn validate_journaled_memory_config(
793    errs: &mut ErrorTree,
794    store: &Store,
795    config: StoreJournaledMemoryConfig,
796    canister: &Canister,
797) {
798    validate_stable_memory_role(
799        errs,
800        "data_memory_id",
801        "data stable key",
802        config.data_memory_id(),
803        store
804            .stable_data_allocation(canister.memory_namespace())
805            .stable_key(),
806        canister,
807    );
808    validate_stable_memory_role(
809        errs,
810        "index_memory_id",
811        "index stable key",
812        config.index_memory_id(),
813        store
814            .stable_index_allocation(canister.memory_namespace())
815            .stable_key(),
816        canister,
817    );
818    validate_stable_memory_role(
819        errs,
820        "schema_memory_id",
821        "schema stable key",
822        config.schema_memory_id(),
823        store
824            .stable_schema_allocation(canister.memory_namespace())
825            .stable_key(),
826        canister,
827    );
828    validate_stable_memory_role(
829        errs,
830        "journal_memory_id",
831        "journal stable key",
832        config.journal_memory_id(),
833        store
834            .journal_allocation(canister.memory_namespace())
835            .stable_key(),
836        canister,
837    );
838
839    validate_distinct_journaled_memory_ids(errs, config);
840}
841
842fn validate_distinct_journaled_memory_ids(
843    errs: &mut ErrorTree,
844    config: StoreJournaledMemoryConfig,
845) {
846    let roles = [
847        ("data_memory_id", config.data_memory_id()),
848        ("index_memory_id", config.index_memory_id()),
849        ("schema_memory_id", config.schema_memory_id()),
850        ("journal_memory_id", config.journal_memory_id()),
851    ];
852
853    for (idx, (left_label, left_id)) in roles.iter().enumerate() {
854        for (right_label, right_id) in roles.iter().skip(idx + 1) {
855            if left_id == right_id {
856                err!(
857                    errs,
858                    "{} and {} must differ (both are {})",
859                    left_label,
860                    right_label,
861                    left_id,
862                );
863            }
864        }
865    }
866}
867
868fn validate_stable_memory_role(
869    errs: &mut ErrorTree,
870    memory_label: &str,
871    stable_key_label: &str,
872    memory_id: u8,
873    stable_key: &str,
874    canister: &Canister,
875) {
876    validate_memory_id_in_range(
877        errs,
878        memory_label,
879        memory_id,
880        canister.memory_min(),
881        canister.memory_max(),
882    );
883    validate_app_memory_id(errs, memory_label, memory_id);
884    validate_memory_id_not_reserved(errs, memory_label, memory_id);
885    validate_stable_key(errs, stable_key_label, stable_key);
886}
887
888impl VisitableNode for Store {
889    fn route_key(&self) -> String {
890        self.def().path()
891    }
892
893    fn drive<V: Visitor>(&self, v: &mut V) {
894        self.def().accept(v);
895    }
896}
897
898#[cfg(test)]
899mod tests {
900    use crate::{
901        build::schema_write,
902        node::{Canister, SchemaNode},
903    };
904
905    use super::*;
906
907    fn insert_canister(path_module: &'static str, ident: &'static str) {
908        schema_write().insert_node(SchemaNode::Canister(Canister::new(
909            Def::new(path_module, ident),
910            "test_db",
911            100,
912            254,
913            254,
914        )));
915    }
916
917    #[test]
918    fn store_allocations_default_to_absent_schema_metadata() {
919        let store = Store::new_journaled(
920            Def::new("demo::rpg", "CharacterStore"),
921            "CHARACTER_STORE",
922            "characters",
923            "demo::rpg::Canister",
924            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
925        );
926
927        for allocation in [
928            store.stable_data_allocation("demo_rpg"),
929            store.stable_index_allocation("demo_rpg"),
930            store.stable_schema_allocation("demo_rpg"),
931            store.journal_allocation("demo_rpg"),
932        ] {
933            assert_eq!(allocation.schema_version(), None);
934            assert_eq!(allocation.schema_fingerprint_method_version(), None);
935            assert_eq!(allocation.schema_fingerprint(), None);
936            assert_eq!(
937                allocation.schema_metadata(),
938                &StableMemoryAllocationMetadata::absent()
939            );
940        }
941    }
942
943    #[test]
944    fn allocation_metadata_is_role_specific_and_diagnostic_only() {
945        let store = Store::new_journaled(
946            Def::new("demo::rpg", "CharacterStore"),
947            "CHARACTER_STORE",
948            "characters",
949            "demo::rpg::Canister",
950            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
951        );
952        let data = store.stable_data_allocation_with_schema_metadata(
953            "demo_rpg",
954            StableMemoryAllocationMetadata::from_accepted_schema_contract(
955                7,
956                2,
957                "data-row-layout".to_string(),
958            ),
959        );
960        let index = store.stable_index_allocation_with_schema_metadata(
961            "demo_rpg",
962            StableMemoryAllocationMetadata::from_accepted_schema_contract(
963                8,
964                3,
965                "index-catalog".to_string(),
966            ),
967        );
968        let schema = store.stable_schema_allocation_with_schema_metadata(
969            "demo_rpg",
970            StableMemoryAllocationMetadata::from_accepted_schema_contract(
971                10,
972                1,
973                "schema-catalog".to_string(),
974            ),
975        );
976        let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
977            "demo_rpg",
978            StableMemoryAllocationMetadata::from_accepted_schema_contract(
979                9,
980                2,
981                "data-row-layout-v2".to_string(),
982            ),
983        );
984
985        assert_eq!(data.schema_version(), Some(7));
986        assert_eq!(data.schema_fingerprint_method_version(), Some(2));
987        assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
988        assert_eq!(index.schema_version(), Some(8));
989        assert_eq!(index.schema_fingerprint_method_version(), Some(3));
990        assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
991        assert_eq!(schema.schema_version(), Some(10));
992        assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
993        assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
994        assert!(data.same_identity_as(&data_after_reconcile));
995        assert!(!data.same_identity_as(&index));
996        assert!(!data.same_identity_as(&schema));
997    }
998
999    #[test]
1000    fn store_owns_explicit_heap_storage_config() {
1001        insert_canister("store_heap_config", "Canister");
1002        let store = Store::new_heap(
1003            Def::new("store_heap_config", "Store"),
1004            "STORE",
1005            "heap_store",
1006            "store_heap_config::Canister",
1007            StoreHeapConfig::new(),
1008        );
1009
1010        assert!(store.is_heap_storage());
1011        assert!(store.validate().is_ok());
1012    }
1013
1014    #[test]
1015    fn heap_store_storage_capabilities_describe_volatile_contract() {
1016        let store = Store::new_heap(
1017            Def::new("store_heap_capabilities", "Store"),
1018            "STORE",
1019            "heap_store",
1020            "store_heap_capabilities::Canister",
1021            StoreHeapConfig::new(),
1022        );
1023        let capabilities = store.storage_capabilities();
1024
1025        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
1026        assert_eq!(
1027            capabilities.allocation_identity(),
1028            AllocationIdentityCapability::Absent,
1029        );
1030        assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1031        assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1032        assert_eq!(
1033            capabilities.commit_participation(),
1034            CommitParticipation::LiveOnly,
1035        );
1036        assert_eq!(
1037            capabilities.schema_metadata(),
1038            SchemaMetadataCapability::LiveRebuiltMetadata,
1039        );
1040        assert_eq!(
1041            capabilities.relation_source(),
1042            RelationSourceCapability::LiveSource,
1043        );
1044        assert_eq!(
1045            capabilities.relation_target(),
1046            RelationTargetCapability::VolatileTarget,
1047        );
1048        assert_eq!(
1049            capabilities.live_validation(),
1050            LiveValidationCapability::Supported,
1051        );
1052        assert!(!capabilities.has_allocation_identity());
1053        assert!(!capabilities.participates_in_durable_commit());
1054        assert!(capabilities.is_volatile());
1055    }
1056
1057    #[test]
1058    fn store_owns_explicit_journaled_storage_config() {
1059        insert_canister("store_journaled_config", "Canister");
1060        let store = Store::new_journaled(
1061            Def::new("store_journaled_config", "Store"),
1062            "STORE",
1063            "journaled_store",
1064            "store_journaled_config::Canister",
1065            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1066        );
1067
1068        assert!(store.is_journaled_storage());
1069        assert!(!store.is_heap_storage());
1070        let journaled = store
1071            .journaled_memory_config()
1072            .expect("journaled model stores four-role config explicitly");
1073
1074        assert_eq!(journaled.data_memory_id(), 110);
1075        assert_eq!(journaled.index_memory_id(), 111);
1076        assert_eq!(journaled.schema_memory_id(), 112);
1077        assert_eq!(journaled.journal_memory_id(), 113);
1078        assert_eq!(store.stable_data_memory_id(), 110);
1079        assert_eq!(store.stable_index_memory_id(), 111);
1080        assert_eq!(store.stable_schema_memory_id(), 112);
1081        assert_eq!(store.journal_memory_id(), 113);
1082        assert!(store.validate().is_ok());
1083    }
1084
1085    #[test]
1086    fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
1087        let store = Store::new_journaled(
1088            Def::new("store_journaled_capabilities", "Store"),
1089            "STORE",
1090            "journaled_store",
1091            "store_journaled_capabilities::Canister",
1092            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1093        );
1094        let capabilities = store.storage_capabilities();
1095
1096        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1097        assert_eq!(
1098            capabilities.allocation_identity(),
1099            AllocationIdentityCapability::Present,
1100        );
1101        assert_eq!(capabilities.durability(), StoreDurability::Durable);
1102        assert_eq!(
1103            capabilities.recovery(),
1104            StoreRecoveryCapability::StableBasePlusJournalReplay,
1105        );
1106        assert_eq!(
1107            capabilities.commit_participation(),
1108            CommitParticipation::Durable,
1109        );
1110        assert_eq!(
1111            capabilities.schema_metadata(),
1112            SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1113        );
1114        assert_eq!(
1115            capabilities.relation_source(),
1116            RelationSourceCapability::DurableSource,
1117        );
1118        assert_eq!(
1119            capabilities.relation_target(),
1120            RelationTargetCapability::DurableTarget,
1121        );
1122        assert_eq!(
1123            capabilities.live_validation(),
1124            LiveValidationCapability::Supported,
1125        );
1126        assert!(capabilities.has_allocation_identity());
1127        assert!(capabilities.participates_in_durable_commit());
1128        assert!(!capabilities.is_volatile());
1129    }
1130
1131    #[test]
1132    fn journaled_store_allocations_use_role_named_stable_keys() {
1133        let store = Store::new_journaled(
1134            Def::new("demo::rpg", "CharacterStore"),
1135            "CHARACTER_STORE",
1136            "characters",
1137            "demo::rpg::Canister",
1138            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1139        );
1140
1141        assert_eq!(
1142            store.stable_data_allocation("demo_rpg").stable_key(),
1143            "icydb.demo_rpg.characters.data.v1",
1144        );
1145        assert_eq!(
1146            store.stable_index_allocation("demo_rpg").stable_key(),
1147            "icydb.demo_rpg.characters.index.v1",
1148        );
1149        assert_eq!(
1150            store.stable_schema_allocation("demo_rpg").stable_key(),
1151            "icydb.demo_rpg.characters.schema.v1",
1152        );
1153        assert_eq!(
1154            store.journal_allocation("demo_rpg").stable_key(),
1155            "icydb.demo_rpg.characters.journal.v1",
1156        );
1157    }
1158
1159    #[test]
1160    fn storage_capabilities_are_not_allocation_identity() {
1161        let store_a = Store::new_journaled(
1162            Def::new("demo::rpg", "CharacterStore"),
1163            "CHARACTER_STORE",
1164            "characters",
1165            "demo::rpg::Canister",
1166            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1167        );
1168        let store_b = Store::new_journaled(
1169            Def::new("demo::rpg", "InventoryStore"),
1170            "INVENTORY_STORE",
1171            "inventory",
1172            "demo::rpg::Canister",
1173            StoreJournaledMemoryConfig::new(120, 121, 122, 123),
1174        );
1175
1176        assert_eq!(
1177            store_a.storage_capabilities(),
1178            store_b.storage_capabilities()
1179        );
1180        assert_ne!(
1181            store_a.stable_data_allocation("demo_rpg"),
1182            store_b.stable_data_allocation("demo_rpg"),
1183            "stable allocation identity must remain separate from capabilities",
1184        );
1185    }
1186
1187    #[test]
1188    fn capability_consumers_use_axes_not_storage_mode() {
1189        const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1190            match capabilities.commit_participation() {
1191                CommitParticipation::Durable => "durable",
1192                CommitParticipation::LiveOnly => "live-only",
1193            }
1194        }
1195
1196        let future_durable_heap_mode = StoreStorageCapabilities {
1197            storage_mode: StoreStorageMode::Heap,
1198            allocation_identity: AllocationIdentityCapability::Present,
1199            durability: StoreDurability::Durable,
1200            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1201            commit_participation: CommitParticipation::Durable,
1202            schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1203            relation_source: RelationSourceCapability::DurableSource,
1204            relation_target: RelationTargetCapability::DurableTarget,
1205            live_validation: LiveValidationCapability::Supported,
1206        };
1207
1208        assert_eq!(commit_label(future_durable_heap_mode), "durable");
1209        assert!(future_durable_heap_mode.participates_in_durable_commit());
1210        assert_eq!(
1211            future_durable_heap_mode.storage_mode(),
1212            StoreStorageMode::Heap,
1213            "the diagnostic storage mode must not drive commit policy",
1214        );
1215    }
1216
1217    #[test]
1218    fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1219        insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1220        let store = Store::new_journaled(
1221            Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1222            "STORE",
1223            "duplicate_journaled_role_memory_ids",
1224            "store_duplicate_journaled_role_memory_ids::Canister",
1225            StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1226        );
1227
1228        let err = store
1229            .validate()
1230            .expect_err("duplicate journaled role memory IDs must fail validation");
1231        let rendered = err.to_string();
1232
1233        assert!(
1234            rendered.contains("schema_memory_id and journal_memory_id must differ"),
1235            "expected duplicate journaled role memory-id error, got: {rendered}"
1236        );
1237    }
1238}