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