Skip to main content

icydb_model/node/
store.rs

1use crate::node::{validate_stable_key, validate_stable_key_segment};
2use crate::prelude::*;
3
4///
5/// Store
6///
7/// Schema node describing the storage mode for:
8/// - primary entity data
9/// - all index data for that entity
10/// - schema metadata for that store
11///
12
13#[derive(Clone, Debug, Serialize)]
14pub struct Store {
15    def: Def,
16    canister: &'static str,
17    storage: StoreStorage,
18}
19
20/// Storage configuration owned by one schema store declaration.
21///
22/// Store storage has two public modes: volatile heap storage and journaled
23/// cached-stable durable storage.
24///
25/// Use `Journaled` for user data that must survive upgrade/reinitialization.
26/// `Heap` is live-only process state: it has no stable-memory allocation
27/// identity, no commit-marker or journal-tail participation, and no recovery
28/// path.
29#[derive(Clone, Debug, Serialize)]
30pub enum StoreStorage {
31    /// Volatile heap store with no stable allocation identity or recovery path.
32    Heap(StoreHeapConfig),
33    /// Journaled cached-stable store using canonical stable data/index/schema
34    /// memories plus a durable journal-tail memory.
35    Journaled(StoreJournaledMemoryConfig),
36}
37
38impl StoreStorage {
39    /// Borrow the journaled cached-stable configuration.
40    #[must_use]
41    pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
42        match self {
43            Self::Journaled(config) => Some(config),
44            Self::Heap(_) => None,
45        }
46    }
47
48    /// Return the capability descriptor derived from this storage mode.
49    #[must_use]
50    pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
51        match self {
52            Self::Heap(_) => StoreStorageCapabilities::heap(),
53            Self::Journaled(_) => StoreStorageCapabilities::journaled(),
54        }
55    }
56}
57
58/// Diagnostic storage mode carried by a storage capability descriptor.
59///
60/// Policy code should branch on capability axes instead of this display value.
61#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
62pub enum StoreStorageMode {
63    /// Volatile in-process heap storage.
64    Heap,
65    /// Journaled cached-stable durable storage.
66    Journaled,
67}
68
69/// Whether a store storage mode owns durable stable-memory allocation identity.
70#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
71pub enum AllocationIdentityCapability {
72    /// Stable allocation identity is present.
73    Present,
74    /// Stable allocation identity is absent.
75    Absent,
76}
77
78/// Store durability class.
79#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
80pub enum StoreDurability {
81    /// Store contents participate in durable storage semantics.
82    Durable,
83    /// Store contents are live-only and volatile.
84    Volatile,
85}
86
87/// Store recovery capability.
88#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
89pub enum StoreRecoveryCapability {
90    /// Store contents recover from canonical stable BTrees plus committed
91    /// journal tail replay.
92    StableBasePlusJournalReplay,
93    /// Store contents are not recovered.
94    None,
95}
96
97/// Store commit participation class.
98#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
99pub enum CommitParticipation {
100    /// Store mutations participate in the durable commit path.
101    Durable,
102    /// Store mutations are live-only side effects.
103    LiveOnly,
104}
105
106/// Store schema metadata persistence class.
107#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
108pub enum SchemaMetadataCapability {
109    /// The store-local projection is rebuilt from a durable accepted checkpoint
110    /// and does not retain its own schema history.
111    LiveRebuiltMetadata,
112    /// Schema metadata is canonical stable history plus committed journal tail.
113    CanonicalStableHistoryPlusJournalTail,
114}
115
116/// 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/// 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 is the permanent 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/// Permanent store key 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    key: &'static str,
285}
286
287impl StoreJournaledMemoryConfig {
288    /// Name this store permanently within its canister namespace.
289    #[must_use]
290    pub const fn new(key: &'static str) -> Self {
291        Self { key }
292    }
293
294    /// Durable logical identity, independent of Rust names and physical placement.
295    #[must_use]
296    pub const fn key(self) -> &'static str {
297        self.key
298    }
299}
300
301impl Store {
302    /// Build a heap-backed volatile store declaration.
303    #[must_use]
304    pub const fn new_heap(def: Def, canister: &'static str, heap: StoreHeapConfig) -> Self {
305        Self {
306            def,
307            canister,
308            storage: StoreStorage::Heap(heap),
309        }
310    }
311
312    /// Build a journaled cached-stable store declaration.
313    #[must_use]
314    pub const fn new_journaled(
315        def: Def,
316        canister: &'static str,
317        journaled: StoreJournaledMemoryConfig,
318    ) -> Self {
319        Self {
320            def,
321            canister,
322            storage: StoreStorage::Journaled(journaled),
323        }
324    }
325
326    #[must_use]
327    pub const fn def(&self) -> &Def {
328        &self.def
329    }
330
331    #[must_use]
332    pub const fn canister(&self) -> &'static str {
333        self.canister
334    }
335
336    /// Borrow this store's storage configuration.
337    #[must_use]
338    pub const fn storage(&self) -> &StoreStorage {
339        &self.storage
340    }
341
342    /// Return whether this store is heap-backed and volatile.
343    #[must_use]
344    pub const fn is_heap_storage(&self) -> bool {
345        matches!(self.storage, StoreStorage::Heap(_))
346    }
347
348    /// Return whether this store is journaled cached-stable.
349    #[must_use]
350    pub const fn is_journaled_storage(&self) -> bool {
351        matches!(self.storage, StoreStorage::Journaled(_))
352    }
353
354    /// Borrow the journaled store's logical allocation configuration when it uses
355    /// journaled storage.
356    #[must_use]
357    pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
358        self.storage.journaled_memory_config()
359    }
360
361    /// Return the capability descriptor derived from this store's storage mode.
362    #[must_use]
363    pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
364        self.storage.storage_capabilities()
365    }
366
367    /// Return the permanent key for journaled storage.
368    ///
369    /// # Panics
370    ///
371    /// Panics when this store uses heap storage.
372    #[must_use]
373    pub const fn stable_store_key(&self) -> &'static str {
374        match self.storage {
375            StoreStorage::Journaled(config) => config.key(),
376            StoreStorage::Heap(_) => panic!("heap stores do not have a stable store key"),
377        }
378    }
379
380    /// Build the data-memory allocation descriptor for journaled storage.
381    ///
382    /// # Panics
383    ///
384    /// Panics when this store uses heap storage.
385    #[must_use]
386    pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
387        self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
388    }
389
390    /// Build the data-memory allocation descriptor with accepted row-layout
391    /// schema metadata attached for diagnostics.
392    ///
393    /// # Panics
394    ///
395    /// Panics when this store uses heap storage.
396    #[must_use]
397    pub fn stable_data_allocation_with_schema_metadata(
398        &self,
399        memory_namespace: &str,
400        schema_metadata: StableMemoryAllocationMetadata,
401    ) -> StableMemoryAllocation {
402        self.stable_allocation_with_schema_metadata(
403            memory_namespace,
404            StoreMemoryRole::Data,
405            schema_metadata,
406        )
407    }
408
409    /// Build the index-memory allocation descriptor for journaled storage.
410    ///
411    /// # Panics
412    ///
413    /// Panics when this store uses heap storage.
414    #[must_use]
415    pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
416        self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
417    }
418
419    /// Build the index-memory allocation descriptor with accepted index-catalog
420    /// schema metadata attached for diagnostics.
421    ///
422    /// # Panics
423    ///
424    /// Panics when this store uses heap storage.
425    #[must_use]
426    pub fn stable_index_allocation_with_schema_metadata(
427        &self,
428        memory_namespace: &str,
429        schema_metadata: StableMemoryAllocationMetadata,
430    ) -> StableMemoryAllocation {
431        self.stable_allocation_with_schema_metadata(
432            memory_namespace,
433            StoreMemoryRole::Index,
434            schema_metadata,
435        )
436    }
437
438    /// Build the schema-memory allocation descriptor for journaled storage.
439    ///
440    /// # Panics
441    ///
442    /// Panics when this store uses heap storage.
443    #[must_use]
444    pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
445        self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
446    }
447
448    /// Build the journal-tail allocation descriptor for journaled stores.
449    ///
450    /// # Panics
451    ///
452    /// Panics when this store uses heap storage.
453    #[must_use]
454    pub fn journal_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
455        StableMemoryAllocation::without_schema_metadata(stable_store_memory_key(
456            memory_namespace,
457            self.stable_store_key(),
458            "journal",
459        ))
460    }
461
462    /// Build the schema-memory allocation descriptor with accepted catalog
463    /// schema metadata attached for diagnostics.
464    ///
465    /// # Panics
466    ///
467    /// Panics when this store uses heap storage.
468    #[must_use]
469    pub fn stable_schema_allocation_with_schema_metadata(
470        &self,
471        memory_namespace: &str,
472        schema_metadata: StableMemoryAllocationMetadata,
473    ) -> StableMemoryAllocation {
474        self.stable_allocation_with_schema_metadata(
475            memory_namespace,
476            StoreMemoryRole::Schema,
477            schema_metadata,
478        )
479    }
480
481    /// Build an allocation descriptor for one role in journaled storage.
482    ///
483    /// Use [`Self::is_journaled_storage`] or [`Self::journaled_memory_config`]
484    /// to check the storage mode before requesting a stable allocation.
485    ///
486    /// # Panics
487    ///
488    /// Panics when this store uses heap storage.
489    #[must_use]
490    pub fn stable_allocation(
491        &self,
492        memory_namespace: &str,
493        role: StoreMemoryRole,
494    ) -> StableMemoryAllocation {
495        StableMemoryAllocation::without_schema_metadata(stable_store_memory_key(
496            memory_namespace,
497            self.stable_store_key(),
498            role.as_str(),
499        ))
500    }
501
502    fn stable_allocation_with_schema_metadata(
503        &self,
504        memory_namespace: &str,
505        role: StoreMemoryRole,
506        schema_metadata: StableMemoryAllocationMetadata,
507    ) -> StableMemoryAllocation {
508        StableMemoryAllocation::with_schema_metadata(
509            stable_store_memory_key(memory_namespace, self.stable_store_key(), role.as_str()),
510            schema_metadata,
511        )
512    }
513}
514
515#[derive(Clone, Copy, Debug, Eq, PartialEq)]
516pub enum StoreMemoryRole {
517    Data,
518    Index,
519    Schema,
520}
521
522impl StoreMemoryRole {
523    #[must_use]
524    pub const fn as_str(self) -> &'static str {
525        match self {
526            Self::Data => "data",
527            Self::Index => "index",
528            Self::Schema => "schema",
529        }
530    }
531}
532
533/// Diagnostic schema metadata associated with a stable-memory allocation.
534///
535/// This metadata does not participate in durable allocation identity. The
536/// durable identity remains the permanent stable key.
537#[derive(Clone, Debug, Eq, PartialEq)]
538pub struct StableMemoryAllocationMetadata {
539    version: Option<u32>,
540    fingerprint_method_version: Option<u8>,
541    fingerprint: Option<String>,
542}
543
544impl StableMemoryAllocationMetadata {
545    const fn new(
546        schema_version: Option<u32>,
547        schema_fingerprint_method_version: Option<u8>,
548        schema_fingerprint: Option<String>,
549    ) -> Self {
550        Self {
551            version: schema_version,
552            fingerprint_method_version: schema_fingerprint_method_version,
553            fingerprint: schema_fingerprint,
554        }
555    }
556
557    /// Build allocation metadata from an accepted schema/catalog authority.
558    #[must_use]
559    pub const fn from_accepted_schema_contract(
560        schema_version: u32,
561        schema_fingerprint_method_version: u8,
562        schema_fingerprint: String,
563    ) -> Self {
564        Self::new(
565            Some(schema_version),
566            Some(schema_fingerprint_method_version),
567            Some(schema_fingerprint),
568        )
569    }
570
571    /// Build absent allocation metadata for allocations with no accepted
572    /// schema/catalog authority.
573    #[must_use]
574    pub const fn absent() -> Self {
575        Self::new(None, None, None)
576    }
577
578    /// Accepted schema/catalog version, when known.
579    #[must_use]
580    pub const fn schema_version(&self) -> Option<u32> {
581        self.version
582    }
583
584    /// Accepted schema/catalog fingerprint method version, when known.
585    #[must_use]
586    pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
587        self.fingerprint_method_version
588    }
589
590    /// Accepted schema/catalog fingerprint, when known.
591    #[must_use]
592    pub const fn schema_fingerprint(&self) -> Option<&str> {
593        match &self.fingerprint {
594            Some(value) => Some(value.as_str()),
595            None => None,
596        }
597    }
598}
599
600/// Stable-memory allocation descriptor.
601///
602/// The stable key is the durable allocation identity; physical placement is
603/// resolved by the runtime, not the schema model.
604/// `schema_version + schema_fingerprint_method_version + schema_fingerprint`
605/// is diagnostic metadata only.
606#[derive(Clone, Debug, Eq, PartialEq)]
607pub struct StableMemoryAllocation {
608    stable_key: String,
609    schema_metadata: StableMemoryAllocationMetadata,
610}
611
612impl StableMemoryAllocation {
613    /// Build an allocation descriptor without schema metadata.
614    #[must_use]
615    pub const fn without_schema_metadata(stable_key: String) -> Self {
616        Self::with_schema_metadata(stable_key, StableMemoryAllocationMetadata::absent())
617    }
618
619    /// Build an allocation descriptor with diagnostic schema metadata.
620    ///
621    /// The metadata must come from accepted schema/catalog authority. Generated
622    /// model fallback metadata is not an allocation metadata authority.
623    #[must_use]
624    pub const fn with_schema_metadata(
625        stable_key: String,
626        schema_metadata: StableMemoryAllocationMetadata,
627    ) -> Self {
628        Self {
629            stable_key,
630            schema_metadata,
631        }
632    }
633
634    /// Durable stable-memory key.
635    #[must_use]
636    pub const fn stable_key(&self) -> &str {
637        self.stable_key.as_str()
638    }
639
640    /// Diagnostic schema/catalog metadata.
641    #[must_use]
642    pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
643        &self.schema_metadata
644    }
645
646    /// Accepted schema/catalog version, when known.
647    #[must_use]
648    pub const fn schema_version(&self) -> Option<u32> {
649        self.schema_metadata.schema_version()
650    }
651
652    /// Accepted schema/catalog fingerprint method version, when known.
653    #[must_use]
654    pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
655        self.schema_metadata.schema_fingerprint_method_version()
656    }
657
658    /// Accepted schema/catalog fingerprint, when known.
659    #[must_use]
660    pub const fn schema_fingerprint(&self) -> Option<&str> {
661        self.schema_metadata.schema_fingerprint()
662    }
663
664    /// Compare durable allocation identity only.
665    ///
666    /// Schema metadata is intentionally ignored because metadata changes are
667    /// diagnostics, not memory replacement.
668    #[must_use]
669    pub fn same_identity_as(&self, other: &Self) -> bool {
670        self.stable_key == other.stable_key
671    }
672}
673
674#[must_use]
675pub fn stable_memory_key(memory_namespace: &str, allocation: &str, role: &str) -> String {
676    format!("icydb.{memory_namespace}.{allocation}.{role}.v1")
677}
678
679#[must_use]
680fn stable_store_memory_key(memory_namespace: &str, store_key: &str, role: &str) -> String {
681    // Explicit logical identity survives source-level renames and declaration order.
682    format!("icydb.{memory_namespace}.store.{store_key}.{role}.v1")
683}
684
685impl MacroNode for Store {
686    fn as_any(&self) -> &dyn std::any::Any {
687        self
688    }
689}
690
691impl ValidateNode for Store {
692    fn validate(&self) -> Result<(), ErrorTree> {
693        let mut errs = ErrorTree::new();
694
695        {
696            let schema = schema_read();
697
698            match schema.cast_node::<Canister>(self.canister()) {
699                Ok(canister) => match self.storage() {
700                    StoreStorage::Heap(_) => {}
701                    StoreStorage::Journaled(config) => {
702                        validate_journaled_memory_config(&mut errs, self, *config, canister);
703                    }
704                },
705                Err(e) => errs.add(e),
706            }
707        }
708
709        errs.result()
710    }
711}
712
713fn validate_journaled_memory_config(
714    errs: &mut ErrorTree,
715    store: &Store,
716    config: StoreJournaledMemoryConfig,
717    canister: &Canister,
718) {
719    validate_stable_key_segment(errs, "store key", config.key());
720    for allocation in [
721        store.stable_data_allocation(canister.memory_namespace()),
722        store.stable_index_allocation(canister.memory_namespace()),
723        store.stable_schema_allocation(canister.memory_namespace()),
724        store.journal_allocation(canister.memory_namespace()),
725    ] {
726        validate_stable_key(errs, "store allocation key", allocation.stable_key());
727    }
728}
729
730impl VisitableNode for Store {
731    fn route_key(&self) -> String {
732        self.def().path()
733    }
734
735    fn drive<V: Visitor>(&self, v: &mut V) {
736        self.def().accept(v);
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use crate::{
743        build::schema_write,
744        node::{Canister, SchemaNode},
745    };
746
747    use super::*;
748
749    fn insert_canister(path_module: &'static str, ident: &'static str) {
750        schema_write().insert_node(SchemaNode::Canister(Canister::new(
751            Def::new(path_module, ident),
752            "test_db",
753            None,
754        )));
755    }
756
757    #[test]
758    fn store_allocations_default_to_absent_schema_metadata() {
759        let store = Store::new_journaled(
760            Def::new("demo::rpg", "CharacterStore"),
761            "demo::rpg::Canister",
762            StoreJournaledMemoryConfig::new("store_110"),
763        );
764
765        for allocation in [
766            store.stable_data_allocation("demo_rpg"),
767            store.stable_index_allocation("demo_rpg"),
768            store.stable_schema_allocation("demo_rpg"),
769            store.journal_allocation("demo_rpg"),
770        ] {
771            assert_eq!(allocation.schema_version(), None);
772            assert_eq!(allocation.schema_fingerprint_method_version(), None);
773            assert_eq!(allocation.schema_fingerprint(), None);
774            assert_eq!(
775                allocation.schema_metadata(),
776                &StableMemoryAllocationMetadata::absent()
777            );
778        }
779    }
780
781    #[test]
782    fn allocation_metadata_is_role_specific_and_diagnostic_only() {
783        let store = Store::new_journaled(
784            Def::new("demo::rpg", "CharacterStore"),
785            "demo::rpg::Canister",
786            StoreJournaledMemoryConfig::new("store_110"),
787        );
788        let data = store.stable_data_allocation_with_schema_metadata(
789            "demo_rpg",
790            StableMemoryAllocationMetadata::from_accepted_schema_contract(
791                7,
792                1,
793                "data-row-layout".to_string(),
794            ),
795        );
796        let index = store.stable_index_allocation_with_schema_metadata(
797            "demo_rpg",
798            StableMemoryAllocationMetadata::from_accepted_schema_contract(
799                8,
800                1,
801                "index-catalog".to_string(),
802            ),
803        );
804        let schema = store.stable_schema_allocation_with_schema_metadata(
805            "demo_rpg",
806            StableMemoryAllocationMetadata::from_accepted_schema_contract(
807                10,
808                1,
809                "schema-catalog".to_string(),
810            ),
811        );
812        let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
813            "demo_rpg",
814            StableMemoryAllocationMetadata::from_accepted_schema_contract(
815                9,
816                1,
817                "data-row-layout-changed".to_string(),
818            ),
819        );
820
821        assert_eq!(data.schema_version(), Some(7));
822        assert_eq!(data.schema_fingerprint_method_version(), Some(1));
823        assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
824        assert_eq!(index.schema_version(), Some(8));
825        assert_eq!(index.schema_fingerprint_method_version(), Some(1));
826        assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
827        assert_eq!(schema.schema_version(), Some(10));
828        assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
829        assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
830        assert!(data.same_identity_as(&data_after_reconcile));
831        assert!(!data.same_identity_as(&index));
832        assert!(!data.same_identity_as(&schema));
833    }
834
835    #[test]
836    fn store_owns_explicit_heap_storage_config() {
837        insert_canister("store_heap_config", "Canister");
838        let store = Store::new_heap(
839            Def::new("store_heap_config", "Store"),
840            "store_heap_config::Canister",
841            StoreHeapConfig::new(),
842        );
843
844        assert!(store.is_heap_storage());
845        assert!(store.validate().is_ok());
846    }
847
848    #[test]
849    fn heap_store_storage_capabilities_describe_volatile_contract() {
850        let store = Store::new_heap(
851            Def::new("store_heap_capabilities", "Store"),
852            "store_heap_capabilities::Canister",
853            StoreHeapConfig::new(),
854        );
855        let capabilities = store.storage_capabilities();
856
857        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
858        assert_eq!(
859            capabilities.allocation_identity(),
860            AllocationIdentityCapability::Absent,
861        );
862        assert_eq!(capabilities.durability(), StoreDurability::Volatile);
863        assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
864        assert_eq!(
865            capabilities.commit_participation(),
866            CommitParticipation::LiveOnly,
867        );
868        assert_eq!(
869            capabilities.schema_metadata(),
870            SchemaMetadataCapability::LiveRebuiltMetadata,
871        );
872        assert_eq!(
873            capabilities.relation_source(),
874            RelationSourceCapability::LiveSource,
875        );
876        assert_eq!(
877            capabilities.relation_target(),
878            RelationTargetCapability::VolatileTarget,
879        );
880        assert_eq!(
881            capabilities.live_validation(),
882            LiveValidationCapability::Supported,
883        );
884        assert!(!capabilities.has_allocation_identity());
885        assert!(!capabilities.participates_in_durable_commit());
886        assert!(capabilities.is_volatile());
887    }
888
889    #[test]
890    fn store_owns_explicit_journaled_storage_config() {
891        insert_canister("store_journaled_config", "Canister");
892        let store = Store::new_journaled(
893            Def::new("store_journaled_config", "Store"),
894            "store_journaled_config::Canister",
895            StoreJournaledMemoryConfig::new("store_110"),
896        );
897
898        assert!(store.is_journaled_storage());
899        assert!(!store.is_heap_storage());
900        let journaled = store
901            .journaled_memory_config()
902            .expect("journaled model stores four-role config explicitly");
903        assert_eq!(journaled.key(), "store_110");
904        assert_eq!(store.stable_store_key(), "store_110");
905        assert!(store.validate().is_ok());
906    }
907
908    #[test]
909    fn store_keys_validate_segments_and_complete_role_key_lengths() {
910        let canister = Canister::new(Def::new("bounded_key", "Canister"), "app", None);
911        // The journal role is longest: prefix + store key + suffix = 27 + key bytes.
912        let admitted = "a".repeat(101);
913        let oversized = "a".repeat(102);
914        for (key, accepted) in [
915            ("transfers", true),
916            ("", false),
917            ("Transfers", false),
918            ("1store", false),
919            ("store.name", false),
920            (admitted.as_str(), true),
921            (oversized.as_str(), false),
922        ] {
923            let config =
924                StoreJournaledMemoryConfig::new(Box::leak(key.to_string().into_boxed_str()));
925            let store = Store::new_journaled(
926                Def::new("bounded_key", "Store"),
927                "bounded_key::Canister",
928                config,
929            );
930            let mut errors = ErrorTree::new();
931            validate_journaled_memory_config(&mut errors, &store, config, &canister);
932            assert_eq!(errors.result().is_ok(), accepted);
933        }
934    }
935
936    #[test]
937    fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
938        let store = Store::new_journaled(
939            Def::new("store_journaled_capabilities", "Store"),
940            "store_journaled_capabilities::Canister",
941            StoreJournaledMemoryConfig::new("store_110"),
942        );
943        let capabilities = store.storage_capabilities();
944
945        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
946        assert_eq!(
947            capabilities.allocation_identity(),
948            AllocationIdentityCapability::Present,
949        );
950        assert_eq!(capabilities.durability(), StoreDurability::Durable);
951        assert_eq!(
952            capabilities.recovery(),
953            StoreRecoveryCapability::StableBasePlusJournalReplay,
954        );
955        assert_eq!(
956            capabilities.commit_participation(),
957            CommitParticipation::Durable,
958        );
959        assert_eq!(
960            capabilities.schema_metadata(),
961            SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
962        );
963        assert_eq!(
964            capabilities.relation_source(),
965            RelationSourceCapability::DurableSource,
966        );
967        assert_eq!(
968            capabilities.relation_target(),
969            RelationTargetCapability::DurableTarget,
970        );
971        assert_eq!(
972            capabilities.live_validation(),
973            LiveValidationCapability::Supported,
974        );
975        assert!(capabilities.has_allocation_identity());
976        assert!(capabilities.participates_in_durable_commit());
977        assert!(!capabilities.is_volatile());
978    }
979
980    #[test]
981    fn journaled_store_allocations_use_role_named_stable_keys() {
982        let store = Store::new_journaled(
983            Def::new("demo::rpg", "CharacterStore"),
984            "demo::rpg::Canister",
985            StoreJournaledMemoryConfig::new("store_110"),
986        );
987
988        assert_eq!(
989            store.stable_data_allocation("demo_rpg").stable_key(),
990            "icydb.demo_rpg.store.store_110.data.v1",
991        );
992        assert_eq!(
993            store.stable_index_allocation("demo_rpg").stable_key(),
994            "icydb.demo_rpg.store.store_110.index.v1",
995        );
996        assert_eq!(
997            store.stable_schema_allocation("demo_rpg").stable_key(),
998            "icydb.demo_rpg.store.store_110.schema.v1",
999        );
1000        assert_eq!(
1001            store.journal_allocation("demo_rpg").stable_key(),
1002            "icydb.demo_rpg.store.store_110.journal.v1",
1003        );
1004
1005        // Rust names locate generated code; only the explicit key names memory.
1006        let renamed = Store::new_journaled(
1007            Def::new("renamed::module", "RenamedStore"),
1008            "renamed::module::RenamedCanister",
1009            StoreJournaledMemoryConfig::new("store_110"),
1010        );
1011        for (before, after) in [
1012            (
1013                store.stable_data_allocation("demo_rpg"),
1014                renamed.stable_data_allocation("demo_rpg"),
1015            ),
1016            (
1017                store.stable_index_allocation("demo_rpg"),
1018                renamed.stable_index_allocation("demo_rpg"),
1019            ),
1020            (
1021                store.stable_schema_allocation("demo_rpg"),
1022                renamed.stable_schema_allocation("demo_rpg"),
1023            ),
1024            (
1025                store.journal_allocation("demo_rpg"),
1026                renamed.journal_allocation("demo_rpg"),
1027            ),
1028        ] {
1029            assert_eq!(before.stable_key(), after.stable_key());
1030        }
1031    }
1032
1033    #[test]
1034    fn storage_capabilities_are_not_allocation_identity() {
1035        let store_a = Store::new_journaled(
1036            Def::new("demo::rpg", "CharacterStore"),
1037            "demo::rpg::Canister",
1038            StoreJournaledMemoryConfig::new("store_110"),
1039        );
1040        let store_b = Store::new_journaled(
1041            Def::new("demo::rpg", "InventoryStore"),
1042            "demo::rpg::Canister",
1043            StoreJournaledMemoryConfig::new("store_120"),
1044        );
1045
1046        assert_eq!(
1047            store_a.storage_capabilities(),
1048            store_b.storage_capabilities()
1049        );
1050        assert_ne!(
1051            store_a.stable_data_allocation("demo_rpg"),
1052            store_b.stable_data_allocation("demo_rpg"),
1053            "stable allocation identity must remain separate from capabilities",
1054        );
1055    }
1056
1057    #[test]
1058    fn capability_consumers_use_axes_not_storage_mode() {
1059        const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1060            match capabilities.commit_participation() {
1061                CommitParticipation::Durable => "durable",
1062                CommitParticipation::LiveOnly => "live-only",
1063            }
1064        }
1065
1066        let future_durable_heap_mode = StoreStorageCapabilities {
1067            storage_mode: StoreStorageMode::Heap,
1068            allocation_identity: AllocationIdentityCapability::Present,
1069            durability: StoreDurability::Durable,
1070            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1071            commit_participation: CommitParticipation::Durable,
1072            schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1073            relation_source: RelationSourceCapability::DurableSource,
1074            relation_target: RelationTargetCapability::DurableTarget,
1075            live_validation: LiveValidationCapability::Supported,
1076        };
1077
1078        assert_eq!(commit_label(future_durable_heap_mode), "durable");
1079        assert!(future_durable_heap_mode.participates_in_durable_commit());
1080        assert_eq!(
1081            future_durable_heap_mode.storage_mode(),
1082            StoreStorageMode::Heap,
1083            "the diagnostic storage mode must not drive commit policy",
1084        );
1085    }
1086}