Skip to main content

icydb_schema/node/
store.rs

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