Skip to main content

icydb_schema/node/
store.rs

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