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    /// Return the stable data-memory ID for journaled storage.
422    ///
423    /// # Panics
424    ///
425    /// Panics when this store uses heap storage.
426    #[must_use]
427    pub const fn stable_data_memory_id(&self) -> u8 {
428        match self.storage {
429            StoreStorage::Journaled(config) => config.data_memory_id(),
430            StoreStorage::Heap(_) => panic!("heap stores do not have a stable data memory id"),
431        }
432    }
433
434    /// Return the stable index-memory ID for journaled storage.
435    ///
436    /// # Panics
437    ///
438    /// Panics when this store uses heap storage.
439    #[must_use]
440    pub const fn stable_index_memory_id(&self) -> u8 {
441        match self.storage {
442            StoreStorage::Journaled(config) => config.index_memory_id(),
443            StoreStorage::Heap(_) => panic!("heap stores do not have a stable index memory id"),
444        }
445    }
446
447    /// Return the stable schema-memory ID for journaled storage.
448    ///
449    /// # Panics
450    ///
451    /// Panics when this store uses heap storage.
452    #[must_use]
453    pub const fn stable_schema_memory_id(&self) -> u8 {
454        match self.storage {
455            StoreStorage::Journaled(config) => config.schema_memory_id(),
456            StoreStorage::Heap(_) => panic!("heap stores do not have a stable schema memory id"),
457        }
458    }
459
460    /// Return the journal memory ID for journaled storage.
461    ///
462    /// # Panics
463    ///
464    /// Panics when this store uses heap storage.
465    #[must_use]
466    pub const fn journal_memory_id(&self) -> u8 {
467        match self.storage {
468            StoreStorage::Journaled(config) => config.journal_memory_id(),
469            StoreStorage::Heap(_) => panic!("heap stores do not have a journal memory id"),
470        }
471    }
472
473    #[must_use]
474    pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
475        self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
476    }
477
478    /// Build the data-memory allocation descriptor with accepted row-layout
479    /// schema metadata attached for diagnostics.
480    #[must_use]
481    pub fn stable_data_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::Data,
489            schema_metadata,
490        )
491    }
492
493    #[must_use]
494    pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
495        self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
496    }
497
498    /// Build the index-memory allocation descriptor with accepted index-catalog
499    /// schema metadata attached for diagnostics.
500    #[must_use]
501    pub fn stable_index_allocation_with_schema_metadata(
502        &self,
503        memory_namespace: &str,
504        schema_metadata: StableMemoryAllocationMetadata,
505    ) -> StableMemoryAllocation {
506        self.stable_allocation_with_schema_metadata(
507            memory_namespace,
508            StoreMemoryRole::Index,
509            schema_metadata,
510        )
511    }
512
513    #[must_use]
514    pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
515        self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
516    }
517
518    /// Build the journal-tail allocation descriptor for journaled stores.
519    #[must_use]
520    pub fn journal_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
521        StableMemoryAllocation::without_schema_metadata(
522            self.journal_memory_id(),
523            stable_memory_key(memory_namespace, self.store_name(), "journal"),
524        )
525    }
526
527    /// Build the schema-memory allocation descriptor with accepted catalog
528    /// schema metadata attached for diagnostics.
529    #[must_use]
530    pub fn stable_schema_allocation_with_schema_metadata(
531        &self,
532        memory_namespace: &str,
533        schema_metadata: StableMemoryAllocationMetadata,
534    ) -> StableMemoryAllocation {
535        self.stable_allocation_with_schema_metadata(
536            memory_namespace,
537            StoreMemoryRole::Schema,
538            schema_metadata,
539        )
540    }
541
542    #[must_use]
543    pub fn stable_allocation(
544        &self,
545        memory_namespace: &str,
546        role: StoreMemoryRole,
547    ) -> StableMemoryAllocation {
548        let memory_id = match role {
549            StoreMemoryRole::Data => self.stable_data_memory_id(),
550            StoreMemoryRole::Index => self.stable_index_memory_id(),
551            StoreMemoryRole::Schema => self.stable_schema_memory_id(),
552        };
553
554        StableMemoryAllocation::without_schema_metadata(
555            memory_id,
556            stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
557        )
558    }
559
560    fn stable_allocation_with_schema_metadata(
561        &self,
562        memory_namespace: &str,
563        role: StoreMemoryRole,
564        schema_metadata: StableMemoryAllocationMetadata,
565    ) -> StableMemoryAllocation {
566        let memory_id = match role {
567            StoreMemoryRole::Data => self.stable_data_memory_id(),
568            StoreMemoryRole::Index => self.stable_index_memory_id(),
569            StoreMemoryRole::Schema => self.stable_schema_memory_id(),
570        };
571
572        StableMemoryAllocation::with_schema_metadata(
573            memory_id,
574            stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
575            schema_metadata,
576        )
577    }
578}
579
580#[derive(Clone, Copy, Debug, Eq, PartialEq)]
581pub enum StoreMemoryRole {
582    Data,
583    Index,
584    Schema,
585}
586
587impl StoreMemoryRole {
588    #[must_use]
589    pub const fn as_str(self) -> &'static str {
590        match self {
591            Self::Data => "data",
592            Self::Index => "index",
593            Self::Schema => "schema",
594        }
595    }
596}
597
598/// Diagnostic schema metadata associated with a stable-memory allocation.
599///
600/// This metadata does not participate in durable allocation identity. The
601/// durable identity remains `memory_id + stable_key`.
602#[derive(Clone, Debug, Eq, PartialEq)]
603pub struct StableMemoryAllocationMetadata {
604    version: Option<u32>,
605    fingerprint_method_version: Option<u8>,
606    fingerprint: Option<String>,
607}
608
609impl StableMemoryAllocationMetadata {
610    const fn new(
611        schema_version: Option<u32>,
612        schema_fingerprint_method_version: Option<u8>,
613        schema_fingerprint: Option<String>,
614    ) -> Self {
615        Self {
616            version: schema_version,
617            fingerprint_method_version: schema_fingerprint_method_version,
618            fingerprint: schema_fingerprint,
619        }
620    }
621
622    /// Build allocation metadata from an accepted schema/catalog authority.
623    #[must_use]
624    pub const fn from_accepted_schema_contract(
625        schema_version: u32,
626        schema_fingerprint_method_version: u8,
627        schema_fingerprint: String,
628    ) -> Self {
629        Self::new(
630            Some(schema_version),
631            Some(schema_fingerprint_method_version),
632            Some(schema_fingerprint),
633        )
634    }
635
636    /// Build absent allocation metadata for allocations with no accepted
637    /// schema/catalog authority.
638    #[must_use]
639    pub const fn absent() -> Self {
640        Self::new(None, None, None)
641    }
642
643    /// Accepted schema/catalog version, when known.
644    #[must_use]
645    pub const fn schema_version(&self) -> Option<u32> {
646        self.version
647    }
648
649    /// Accepted schema/catalog fingerprint method version, when known.
650    #[must_use]
651    pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
652        self.fingerprint_method_version
653    }
654
655    /// Accepted schema/catalog fingerprint, when known.
656    #[must_use]
657    pub const fn schema_fingerprint(&self) -> Option<&str> {
658        match &self.fingerprint {
659            Some(value) => Some(value.as_str()),
660            None => None,
661        }
662    }
663}
664
665/// Stable-memory allocation descriptor.
666///
667/// `memory_id + stable_key` is the durable allocation identity.
668/// `schema_version + schema_fingerprint_method_version + schema_fingerprint`
669/// is diagnostic metadata only.
670#[derive(Clone, Debug, Eq, PartialEq)]
671pub struct StableMemoryAllocation {
672    memory_id: u8,
673    stable_key: String,
674    schema_metadata: StableMemoryAllocationMetadata,
675}
676
677impl StableMemoryAllocation {
678    /// Build an allocation descriptor without schema metadata.
679    #[must_use]
680    pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
681        Self::with_schema_metadata(
682            memory_id,
683            stable_key,
684            StableMemoryAllocationMetadata::absent(),
685        )
686    }
687
688    /// Build an allocation descriptor with diagnostic schema metadata.
689    ///
690    /// The metadata must come from accepted schema/catalog authority. Generated
691    /// model fallback metadata is not an allocation metadata authority.
692    #[must_use]
693    pub const fn with_schema_metadata(
694        memory_id: u8,
695        stable_key: String,
696        schema_metadata: StableMemoryAllocationMetadata,
697    ) -> Self {
698        Self {
699            memory_id,
700            stable_key,
701            schema_metadata,
702        }
703    }
704
705    /// Stable-memory manager ID.
706    #[must_use]
707    pub const fn memory_id(&self) -> u8 {
708        self.memory_id
709    }
710
711    /// Durable stable-memory key.
712    #[must_use]
713    pub const fn stable_key(&self) -> &str {
714        self.stable_key.as_str()
715    }
716
717    /// Diagnostic schema/catalog metadata.
718    #[must_use]
719    pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
720        &self.schema_metadata
721    }
722
723    /// Accepted schema/catalog version, when known.
724    #[must_use]
725    pub const fn schema_version(&self) -> Option<u32> {
726        self.schema_metadata.schema_version()
727    }
728
729    /// Accepted schema/catalog fingerprint method version, when known.
730    #[must_use]
731    pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
732        self.schema_metadata.schema_fingerprint_method_version()
733    }
734
735    /// Accepted schema/catalog fingerprint, when known.
736    #[must_use]
737    pub const fn schema_fingerprint(&self) -> Option<&str> {
738        self.schema_metadata.schema_fingerprint()
739    }
740
741    /// Compare durable allocation identity only.
742    ///
743    /// Schema metadata is intentionally ignored because metadata changes are
744    /// diagnostics, not memory replacement.
745    #[must_use]
746    pub fn same_identity_as(&self, other: &Self) -> bool {
747        self.memory_id == other.memory_id && self.stable_key == other.stable_key
748    }
749}
750
751#[must_use]
752pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
753    format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
754}
755
756impl MacroNode for Store {
757    fn as_any(&self) -> &dyn std::any::Any {
758        self
759    }
760}
761
762impl ValidateNode for Store {
763    fn validate(&self) -> Result<(), ErrorTree> {
764        let mut errs = ErrorTree::new();
765        let schema = schema_read();
766
767        match schema.cast_node::<Canister>(self.canister()) {
768            Ok(canister) => {
769                validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
770                match self.storage() {
771                    StoreStorage::Heap(_) => {}
772                    StoreStorage::Journaled(config) => {
773                        validate_journaled_memory_config(&mut errs, self, *config, canister);
774                    }
775                }
776            }
777            Err(e) => errs.add(e),
778        }
779
780        errs.result()
781    }
782}
783
784fn validate_journaled_memory_config(
785    errs: &mut ErrorTree,
786    store: &Store,
787    config: StoreJournaledMemoryConfig,
788    canister: &Canister,
789) {
790    validate_stable_memory_role(
791        errs,
792        "data_memory_id",
793        "data stable key",
794        config.data_memory_id(),
795        store
796            .stable_data_allocation(canister.memory_namespace())
797            .stable_key(),
798        canister,
799    );
800    validate_stable_memory_role(
801        errs,
802        "index_memory_id",
803        "index stable key",
804        config.index_memory_id(),
805        store
806            .stable_index_allocation(canister.memory_namespace())
807            .stable_key(),
808        canister,
809    );
810    validate_stable_memory_role(
811        errs,
812        "schema_memory_id",
813        "schema stable key",
814        config.schema_memory_id(),
815        store
816            .stable_schema_allocation(canister.memory_namespace())
817            .stable_key(),
818        canister,
819    );
820    validate_stable_memory_role(
821        errs,
822        "journal_memory_id",
823        "journal stable key",
824        config.journal_memory_id(),
825        store
826            .journal_allocation(canister.memory_namespace())
827            .stable_key(),
828        canister,
829    );
830
831    validate_distinct_journaled_memory_ids(errs, config);
832}
833
834fn validate_distinct_journaled_memory_ids(
835    errs: &mut ErrorTree,
836    config: StoreJournaledMemoryConfig,
837) {
838    let roles = [
839        ("data_memory_id", config.data_memory_id()),
840        ("index_memory_id", config.index_memory_id()),
841        ("schema_memory_id", config.schema_memory_id()),
842        ("journal_memory_id", config.journal_memory_id()),
843    ];
844
845    for (idx, (left_label, left_id)) in roles.iter().enumerate() {
846        for (right_label, right_id) in roles.iter().skip(idx + 1) {
847            if left_id == right_id {
848                err!(
849                    errs,
850                    "{} and {} must differ (both are {})",
851                    left_label,
852                    right_label,
853                    left_id,
854                );
855            }
856        }
857    }
858}
859
860fn validate_stable_memory_role(
861    errs: &mut ErrorTree,
862    memory_label: &str,
863    stable_key_label: &str,
864    memory_id: u8,
865    stable_key: &str,
866    canister: &Canister,
867) {
868    validate_memory_id_in_range(
869        errs,
870        memory_label,
871        memory_id,
872        canister.memory_min(),
873        canister.memory_max(),
874    );
875    validate_app_memory_id(errs, memory_label, memory_id);
876    validate_memory_id_not_reserved(errs, memory_label, memory_id);
877    validate_stable_key(errs, stable_key_label, stable_key);
878}
879
880impl VisitableNode for Store {
881    fn route_key(&self) -> String {
882        self.def().path()
883    }
884
885    fn drive<V: Visitor>(&self, v: &mut V) {
886        self.def().accept(v);
887    }
888}
889
890#[cfg(test)]
891mod tests {
892    use crate::{
893        build::schema_write,
894        node::{Canister, SchemaNode},
895    };
896
897    use super::*;
898
899    fn insert_canister(path_module: &'static str, ident: &'static str) {
900        schema_write().insert_node(SchemaNode::Canister(Canister::new(
901            Def::new(path_module, ident),
902            "test_db",
903            100,
904            254,
905            254,
906        )));
907    }
908
909    #[test]
910    fn store_allocations_default_to_absent_schema_metadata() {
911        let store = Store::new_journaled(
912            Def::new("demo::rpg", "CharacterStore"),
913            "CHARACTER_STORE",
914            "characters",
915            "demo::rpg::Canister",
916            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
917        );
918
919        for allocation in [
920            store.stable_data_allocation("demo_rpg"),
921            store.stable_index_allocation("demo_rpg"),
922            store.stable_schema_allocation("demo_rpg"),
923            store.journal_allocation("demo_rpg"),
924        ] {
925            assert_eq!(allocation.schema_version(), None);
926            assert_eq!(allocation.schema_fingerprint_method_version(), None);
927            assert_eq!(allocation.schema_fingerprint(), None);
928            assert_eq!(
929                allocation.schema_metadata(),
930                &StableMemoryAllocationMetadata::absent()
931            );
932        }
933    }
934
935    #[test]
936    fn allocation_metadata_is_role_specific_and_diagnostic_only() {
937        let store = Store::new_journaled(
938            Def::new("demo::rpg", "CharacterStore"),
939            "CHARACTER_STORE",
940            "characters",
941            "demo::rpg::Canister",
942            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
943        );
944        let data = store.stable_data_allocation_with_schema_metadata(
945            "demo_rpg",
946            StableMemoryAllocationMetadata::from_accepted_schema_contract(
947                7,
948                2,
949                "data-row-layout".to_string(),
950            ),
951        );
952        let index = store.stable_index_allocation_with_schema_metadata(
953            "demo_rpg",
954            StableMemoryAllocationMetadata::from_accepted_schema_contract(
955                8,
956                3,
957                "index-catalog".to_string(),
958            ),
959        );
960        let schema = store.stable_schema_allocation_with_schema_metadata(
961            "demo_rpg",
962            StableMemoryAllocationMetadata::from_accepted_schema_contract(
963                10,
964                1,
965                "schema-catalog".to_string(),
966            ),
967        );
968        let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
969            "demo_rpg",
970            StableMemoryAllocationMetadata::from_accepted_schema_contract(
971                9,
972                2,
973                "data-row-layout-v2".to_string(),
974            ),
975        );
976
977        assert_eq!(data.schema_version(), Some(7));
978        assert_eq!(data.schema_fingerprint_method_version(), Some(2));
979        assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
980        assert_eq!(index.schema_version(), Some(8));
981        assert_eq!(index.schema_fingerprint_method_version(), Some(3));
982        assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
983        assert_eq!(schema.schema_version(), Some(10));
984        assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
985        assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
986        assert!(data.same_identity_as(&data_after_reconcile));
987        assert!(!data.same_identity_as(&index));
988        assert!(!data.same_identity_as(&schema));
989    }
990
991    #[test]
992    fn store_owns_explicit_heap_storage_config() {
993        insert_canister("store_heap_config", "Canister");
994        let store = Store::new_heap(
995            Def::new("store_heap_config", "Store"),
996            "STORE",
997            "heap_store",
998            "store_heap_config::Canister",
999            StoreHeapConfig::new(),
1000        );
1001
1002        assert!(store.is_heap_storage());
1003        assert!(store.validate().is_ok());
1004    }
1005
1006    #[test]
1007    fn heap_store_storage_capabilities_describe_volatile_contract() {
1008        let store = Store::new_heap(
1009            Def::new("store_heap_capabilities", "Store"),
1010            "STORE",
1011            "heap_store",
1012            "store_heap_capabilities::Canister",
1013            StoreHeapConfig::new(),
1014        );
1015        let capabilities = store.storage_capabilities();
1016
1017        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
1018        assert_eq!(
1019            capabilities.allocation_identity(),
1020            AllocationIdentityCapability::Absent,
1021        );
1022        assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1023        assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1024        assert_eq!(
1025            capabilities.commit_participation(),
1026            CommitParticipation::LiveOnly,
1027        );
1028        assert_eq!(
1029            capabilities.schema_metadata(),
1030            SchemaMetadataCapability::LiveRebuiltMetadata,
1031        );
1032        assert_eq!(
1033            capabilities.relation_source(),
1034            RelationSourceCapability::LiveSource,
1035        );
1036        assert_eq!(
1037            capabilities.relation_target(),
1038            RelationTargetCapability::VolatileTarget,
1039        );
1040        assert_eq!(
1041            capabilities.live_validation(),
1042            LiveValidationCapability::Supported,
1043        );
1044        assert!(!capabilities.has_allocation_identity());
1045        assert!(!capabilities.participates_in_durable_commit());
1046        assert!(capabilities.is_volatile());
1047    }
1048
1049    #[test]
1050    fn store_owns_explicit_journaled_storage_config() {
1051        insert_canister("store_journaled_config", "Canister");
1052        let store = Store::new_journaled(
1053            Def::new("store_journaled_config", "Store"),
1054            "STORE",
1055            "journaled_store",
1056            "store_journaled_config::Canister",
1057            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1058        );
1059
1060        assert!(store.is_journaled_storage());
1061        assert!(!store.is_heap_storage());
1062        let journaled = store
1063            .journaled_memory_config()
1064            .expect("journaled model stores four-role config explicitly");
1065
1066        assert_eq!(journaled.data_memory_id(), 110);
1067        assert_eq!(journaled.index_memory_id(), 111);
1068        assert_eq!(journaled.schema_memory_id(), 112);
1069        assert_eq!(journaled.journal_memory_id(), 113);
1070        assert_eq!(store.stable_data_memory_id(), 110);
1071        assert_eq!(store.stable_index_memory_id(), 111);
1072        assert_eq!(store.stable_schema_memory_id(), 112);
1073        assert_eq!(store.journal_memory_id(), 113);
1074        assert!(store.validate().is_ok());
1075    }
1076
1077    #[test]
1078    fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
1079        let store = Store::new_journaled(
1080            Def::new("store_journaled_capabilities", "Store"),
1081            "STORE",
1082            "journaled_store",
1083            "store_journaled_capabilities::Canister",
1084            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1085        );
1086        let capabilities = store.storage_capabilities();
1087
1088        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1089        assert_eq!(
1090            capabilities.allocation_identity(),
1091            AllocationIdentityCapability::Present,
1092        );
1093        assert_eq!(capabilities.durability(), StoreDurability::Durable);
1094        assert_eq!(
1095            capabilities.recovery(),
1096            StoreRecoveryCapability::StableBasePlusJournalReplay,
1097        );
1098        assert_eq!(
1099            capabilities.commit_participation(),
1100            CommitParticipation::Durable,
1101        );
1102        assert_eq!(
1103            capabilities.schema_metadata(),
1104            SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1105        );
1106        assert_eq!(
1107            capabilities.relation_source(),
1108            RelationSourceCapability::DurableSource,
1109        );
1110        assert_eq!(
1111            capabilities.relation_target(),
1112            RelationTargetCapability::DurableTarget,
1113        );
1114        assert_eq!(
1115            capabilities.live_validation(),
1116            LiveValidationCapability::Supported,
1117        );
1118        assert!(capabilities.has_allocation_identity());
1119        assert!(capabilities.participates_in_durable_commit());
1120        assert!(!capabilities.is_volatile());
1121    }
1122
1123    #[test]
1124    fn journaled_store_allocations_use_role_named_stable_keys() {
1125        let store = Store::new_journaled(
1126            Def::new("demo::rpg", "CharacterStore"),
1127            "CHARACTER_STORE",
1128            "characters",
1129            "demo::rpg::Canister",
1130            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1131        );
1132
1133        assert_eq!(
1134            store.stable_data_allocation("demo_rpg").stable_key(),
1135            "icydb.demo_rpg.characters.data.v1",
1136        );
1137        assert_eq!(
1138            store.stable_index_allocation("demo_rpg").stable_key(),
1139            "icydb.demo_rpg.characters.index.v1",
1140        );
1141        assert_eq!(
1142            store.stable_schema_allocation("demo_rpg").stable_key(),
1143            "icydb.demo_rpg.characters.schema.v1",
1144        );
1145        assert_eq!(
1146            store.journal_allocation("demo_rpg").stable_key(),
1147            "icydb.demo_rpg.characters.journal.v1",
1148        );
1149    }
1150
1151    #[test]
1152    fn storage_capabilities_are_not_allocation_identity() {
1153        let store_a = Store::new_journaled(
1154            Def::new("demo::rpg", "CharacterStore"),
1155            "CHARACTER_STORE",
1156            "characters",
1157            "demo::rpg::Canister",
1158            StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1159        );
1160        let store_b = Store::new_journaled(
1161            Def::new("demo::rpg", "InventoryStore"),
1162            "INVENTORY_STORE",
1163            "inventory",
1164            "demo::rpg::Canister",
1165            StoreJournaledMemoryConfig::new(120, 121, 122, 123),
1166        );
1167
1168        assert_eq!(
1169            store_a.storage_capabilities(),
1170            store_b.storage_capabilities()
1171        );
1172        assert_ne!(
1173            store_a.stable_data_allocation("demo_rpg"),
1174            store_b.stable_data_allocation("demo_rpg"),
1175            "stable allocation identity must remain separate from capabilities",
1176        );
1177    }
1178
1179    #[test]
1180    fn capability_consumers_use_axes_not_storage_mode() {
1181        const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1182            match capabilities.commit_participation() {
1183                CommitParticipation::Durable => "durable",
1184                CommitParticipation::LiveOnly => "live-only",
1185            }
1186        }
1187
1188        let future_durable_heap_mode = StoreStorageCapabilities {
1189            storage_mode: StoreStorageMode::Heap,
1190            allocation_identity: AllocationIdentityCapability::Present,
1191            durability: StoreDurability::Durable,
1192            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1193            commit_participation: CommitParticipation::Durable,
1194            schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1195            relation_source: RelationSourceCapability::DurableSource,
1196            relation_target: RelationTargetCapability::DurableTarget,
1197            live_validation: LiveValidationCapability::Supported,
1198        };
1199
1200        assert_eq!(commit_label(future_durable_heap_mode), "durable");
1201        assert!(future_durable_heap_mode.participates_in_durable_commit());
1202        assert_eq!(
1203            future_durable_heap_mode.storage_mode(),
1204            StoreStorageMode::Heap,
1205            "the diagnostic storage mode must not drive commit policy",
1206        );
1207    }
1208
1209    #[test]
1210    fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1211        insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1212        let store = Store::new_journaled(
1213            Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1214            "STORE",
1215            "duplicate_journaled_role_memory_ids",
1216            "store_duplicate_journaled_role_memory_ids::Canister",
1217            StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1218        );
1219
1220        let err = store
1221            .validate()
1222            .expect_err("duplicate journaled role memory IDs must fail validation");
1223        let rendered = err.to_string();
1224
1225        assert!(
1226            rendered.contains("schema_memory_id and journal_memory_id must differ"),
1227            "expected duplicate journaled role memory-id error, got: {rendered}"
1228        );
1229    }
1230}