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. Direct stable-map stores were hard-cut after
24/// the journaled mode became the durable path.
25///
26/// Use `Journaled` for user data that must survive upgrade/reinitialization.
27/// `Heap` is live-only process state: it has no stable-memory allocation
28/// identity, no commit-marker or journal-tail participation, and no recovery
29/// path.
30#[derive(Clone, Debug, Serialize)]
31pub enum StoreStorage {
32    /// Volatile heap store with no stable allocation identity or recovery path.
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    /// The store-local projection is rebuilt from a durable accepted checkpoint
111    /// and does not retain its own schema history.
112    LiveRebuiltMetadata,
113    /// Schema metadata is canonical stable history plus committed journal tail.
114    CanonicalStableHistoryPlusJournalTail,
115}
116
117/// Relation source capability for a store.
118#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
119pub enum RelationSourceCapability {
120    /// Source rows can own durable relation integrity.
121    DurableSource,
122    /// Source rows can participate in live relation validation.
123    LiveSource,
124}
125
126/// Relation target capability for a store.
127#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
128pub enum RelationTargetCapability {
129    /// Target rows can be referenced by durable source rows.
130    DurableTarget,
131    /// Target rows are volatile and cannot satisfy durable source integrity.
132    VolatileTarget,
133}
134
135/// Whether the store can participate in live validation.
136#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
137pub enum LiveValidationCapability {
138    /// Live validation is supported.
139    Supported,
140}
141
142/// Storage capability descriptor derived from a store storage mode.
143///
144/// Capabilities describe storage policy. They are not allocation identity.
145/// Stable allocation identity is the permanent key; heap allocation
146/// identity remains absent.
147#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
148pub struct StoreStorageCapabilities {
149    storage_mode: StoreStorageMode,
150    allocation_identity: AllocationIdentityCapability,
151    durability: StoreDurability,
152    recovery: StoreRecoveryCapability,
153    commit_participation: CommitParticipation,
154    schema_metadata: SchemaMetadataCapability,
155    relation_source: RelationSourceCapability,
156    relation_target: RelationTargetCapability,
157    live_validation: LiveValidationCapability,
158}
159
160impl StoreStorageCapabilities {
161    /// Capability descriptor for heap stores.
162    #[must_use]
163    pub const fn heap() -> Self {
164        Self {
165            storage_mode: StoreStorageMode::Heap,
166            allocation_identity: AllocationIdentityCapability::Absent,
167            durability: StoreDurability::Volatile,
168            recovery: StoreRecoveryCapability::None,
169            commit_participation: CommitParticipation::LiveOnly,
170            schema_metadata: SchemaMetadataCapability::LiveRebuiltMetadata,
171            relation_source: RelationSourceCapability::LiveSource,
172            relation_target: RelationTargetCapability::VolatileTarget,
173            live_validation: LiveValidationCapability::Supported,
174        }
175    }
176
177    /// Capability descriptor for journaled cached-stable stores.
178    #[must_use]
179    pub const fn journaled() -> Self {
180        Self {
181            storage_mode: StoreStorageMode::Journaled,
182            allocation_identity: AllocationIdentityCapability::Present,
183            durability: StoreDurability::Durable,
184            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
185            commit_participation: CommitParticipation::Durable,
186            schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
187            relation_source: RelationSourceCapability::DurableSource,
188            relation_target: RelationTargetCapability::DurableTarget,
189            live_validation: LiveValidationCapability::Supported,
190        }
191    }
192
193    /// Diagnostic storage mode. Policy code should use the capability axes.
194    #[must_use]
195    pub const fn storage_mode(self) -> StoreStorageMode {
196        self.storage_mode
197    }
198
199    /// Stable allocation identity capability.
200    #[must_use]
201    pub const fn allocation_identity(self) -> AllocationIdentityCapability {
202        self.allocation_identity
203    }
204
205    /// Durability capability.
206    #[must_use]
207    pub const fn durability(self) -> StoreDurability {
208        self.durability
209    }
210
211    /// Recovery capability.
212    #[must_use]
213    pub const fn recovery(self) -> StoreRecoveryCapability {
214        self.recovery
215    }
216
217    /// Commit participation capability.
218    #[must_use]
219    pub const fn commit_participation(self) -> CommitParticipation {
220        self.commit_participation
221    }
222
223    /// Schema metadata persistence capability.
224    #[must_use]
225    pub const fn schema_metadata(self) -> SchemaMetadataCapability {
226        self.schema_metadata
227    }
228
229    /// Relation source capability.
230    #[must_use]
231    pub const fn relation_source(self) -> RelationSourceCapability {
232        self.relation_source
233    }
234
235    /// Relation target capability.
236    #[must_use]
237    pub const fn relation_target(self) -> RelationTargetCapability {
238        self.relation_target
239    }
240
241    /// Live validation capability.
242    #[must_use]
243    pub const fn live_validation(self) -> LiveValidationCapability {
244        self.live_validation
245    }
246
247    /// Return whether stable allocation identity is present.
248    #[must_use]
249    pub const fn has_allocation_identity(self) -> bool {
250        matches!(
251            self.allocation_identity,
252            AllocationIdentityCapability::Present
253        )
254    }
255
256    /// Return whether mutations participate in durable commit.
257    #[must_use]
258    pub const fn participates_in_durable_commit(self) -> bool {
259        matches!(self.commit_participation, CommitParticipation::Durable)
260    }
261
262    /// Return whether the store is volatile.
263    #[must_use]
264    pub const fn is_volatile(self) -> bool {
265        matches!(self.durability, StoreDurability::Volatile)
266    }
267}
268
269/// Heap storage configuration for one volatile store.
270#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
271pub struct StoreHeapConfig;
272
273impl StoreHeapConfig {
274    /// Build an empty heap storage configuration.
275    #[must_use]
276    pub const fn new() -> Self {
277        Self
278    }
279}
280
281/// Permanent store key for the four durable roles owned by one journaled
282/// cached-stable store.
283#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
284pub struct StoreJournaledMemoryConfig {
285    key: &'static str,
286}
287
288impl StoreJournaledMemoryConfig {
289    /// Name this store permanently within its canister namespace.
290    #[must_use]
291    pub const fn new(key: &'static str) -> Self {
292        Self { key }
293    }
294
295    /// Durable logical identity, independent of Rust names and physical placement.
296    #[must_use]
297    pub const fn key(self) -> &'static str {
298        self.key
299    }
300}
301
302impl Store {
303    /// Build a heap-backed volatile store declaration.
304    #[must_use]
305    pub const fn new_heap(def: Def, canister: &'static str, heap: StoreHeapConfig) -> Self {
306        Self {
307            def,
308            canister,
309            storage: StoreStorage::Heap(heap),
310        }
311    }
312
313    /// Build a journaled cached-stable store declaration.
314    #[must_use]
315    pub const fn new_journaled(
316        def: Def,
317        canister: &'static str,
318        journaled: StoreJournaledMemoryConfig,
319    ) -> Self {
320        Self {
321            def,
322            canister,
323            storage: StoreStorage::Journaled(journaled),
324        }
325    }
326
327    #[must_use]
328    pub const fn def(&self) -> &Def {
329        &self.def
330    }
331
332    #[must_use]
333    pub const fn canister(&self) -> &'static str {
334        self.canister
335    }
336
337    /// Borrow this store's storage configuration.
338    #[must_use]
339    pub const fn storage(&self) -> &StoreStorage {
340        &self.storage
341    }
342
343    /// Return whether this store is heap-backed and volatile.
344    #[must_use]
345    pub const fn is_heap_storage(&self) -> bool {
346        matches!(self.storage, StoreStorage::Heap(_))
347    }
348
349    /// Return whether this store is journaled cached-stable.
350    #[must_use]
351    pub const fn is_journaled_storage(&self) -> bool {
352        matches!(self.storage, StoreStorage::Journaled(_))
353    }
354
355    /// Borrow the journaled store's logical allocation configuration when it uses
356    /// journaled storage.
357    #[must_use]
358    pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
359        self.storage.journaled_memory_config()
360    }
361
362    /// Return the capability descriptor derived from this store's storage mode.
363    #[must_use]
364    pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
365        self.storage.storage_capabilities()
366    }
367
368    /// Return the permanent key for journaled storage.
369    ///
370    /// # Panics
371    ///
372    /// Panics when this store uses heap storage.
373    #[must_use]
374    pub const fn stable_store_key(&self) -> &'static str {
375        match self.storage {
376            StoreStorage::Journaled(config) => config.key(),
377            StoreStorage::Heap(_) => panic!("heap stores do not have a stable store key"),
378        }
379    }
380
381    #[must_use]
382    pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
383        self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
384    }
385
386    /// Build the data-memory allocation descriptor with accepted row-layout
387    /// schema metadata attached for diagnostics.
388    #[must_use]
389    pub fn stable_data_allocation_with_schema_metadata(
390        &self,
391        memory_namespace: &str,
392        schema_metadata: StableMemoryAllocationMetadata,
393    ) -> StableMemoryAllocation {
394        self.stable_allocation_with_schema_metadata(
395            memory_namespace,
396            StoreMemoryRole::Data,
397            schema_metadata,
398        )
399    }
400
401    #[must_use]
402    pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
403        self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
404    }
405
406    /// Build the index-memory allocation descriptor with accepted index-catalog
407    /// schema metadata attached for diagnostics.
408    #[must_use]
409    pub fn stable_index_allocation_with_schema_metadata(
410        &self,
411        memory_namespace: &str,
412        schema_metadata: StableMemoryAllocationMetadata,
413    ) -> StableMemoryAllocation {
414        self.stable_allocation_with_schema_metadata(
415            memory_namespace,
416            StoreMemoryRole::Index,
417            schema_metadata,
418        )
419    }
420
421    #[must_use]
422    pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
423        self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
424    }
425
426    /// Build the journal-tail allocation descriptor for journaled stores.
427    #[must_use]
428    pub fn journal_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
429        StableMemoryAllocation::without_schema_metadata(stable_store_memory_key(
430            memory_namespace,
431            self.stable_store_key(),
432            "journal",
433        ))
434    }
435
436    /// Build the schema-memory allocation descriptor with accepted catalog
437    /// schema metadata attached for diagnostics.
438    #[must_use]
439    pub fn stable_schema_allocation_with_schema_metadata(
440        &self,
441        memory_namespace: &str,
442        schema_metadata: StableMemoryAllocationMetadata,
443    ) -> StableMemoryAllocation {
444        self.stable_allocation_with_schema_metadata(
445            memory_namespace,
446            StoreMemoryRole::Schema,
447            schema_metadata,
448        )
449    }
450
451    #[must_use]
452    pub fn stable_allocation(
453        &self,
454        memory_namespace: &str,
455        role: StoreMemoryRole,
456    ) -> StableMemoryAllocation {
457        StableMemoryAllocation::without_schema_metadata(stable_store_memory_key(
458            memory_namespace,
459            self.stable_store_key(),
460            role.as_str(),
461        ))
462    }
463
464    fn stable_allocation_with_schema_metadata(
465        &self,
466        memory_namespace: &str,
467        role: StoreMemoryRole,
468        schema_metadata: StableMemoryAllocationMetadata,
469    ) -> StableMemoryAllocation {
470        StableMemoryAllocation::with_schema_metadata(
471            stable_store_memory_key(memory_namespace, self.stable_store_key(), role.as_str()),
472            schema_metadata,
473        )
474    }
475}
476
477#[derive(Clone, Copy, Debug, Eq, PartialEq)]
478pub enum StoreMemoryRole {
479    Data,
480    Index,
481    Schema,
482}
483
484impl StoreMemoryRole {
485    #[must_use]
486    pub const fn as_str(self) -> &'static str {
487        match self {
488            Self::Data => "data",
489            Self::Index => "index",
490            Self::Schema => "schema",
491        }
492    }
493}
494
495/// Diagnostic schema metadata associated with a stable-memory allocation.
496///
497/// This metadata does not participate in durable allocation identity. The
498/// durable identity remains the permanent stable key.
499#[derive(Clone, Debug, Eq, PartialEq)]
500pub struct StableMemoryAllocationMetadata {
501    version: Option<u32>,
502    fingerprint_method_version: Option<u8>,
503    fingerprint: Option<String>,
504}
505
506impl StableMemoryAllocationMetadata {
507    const fn new(
508        schema_version: Option<u32>,
509        schema_fingerprint_method_version: Option<u8>,
510        schema_fingerprint: Option<String>,
511    ) -> Self {
512        Self {
513            version: schema_version,
514            fingerprint_method_version: schema_fingerprint_method_version,
515            fingerprint: schema_fingerprint,
516        }
517    }
518
519    /// Build allocation metadata from an accepted schema/catalog authority.
520    #[must_use]
521    pub const fn from_accepted_schema_contract(
522        schema_version: u32,
523        schema_fingerprint_method_version: u8,
524        schema_fingerprint: String,
525    ) -> Self {
526        Self::new(
527            Some(schema_version),
528            Some(schema_fingerprint_method_version),
529            Some(schema_fingerprint),
530        )
531    }
532
533    /// Build absent allocation metadata for allocations with no accepted
534    /// schema/catalog authority.
535    #[must_use]
536    pub const fn absent() -> Self {
537        Self::new(None, None, None)
538    }
539
540    /// Accepted schema/catalog version, when known.
541    #[must_use]
542    pub const fn schema_version(&self) -> Option<u32> {
543        self.version
544    }
545
546    /// Accepted schema/catalog fingerprint method version, when known.
547    #[must_use]
548    pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
549        self.fingerprint_method_version
550    }
551
552    /// Accepted schema/catalog fingerprint, when known.
553    #[must_use]
554    pub const fn schema_fingerprint(&self) -> Option<&str> {
555        match &self.fingerprint {
556            Some(value) => Some(value.as_str()),
557            None => None,
558        }
559    }
560}
561
562/// Stable-memory allocation descriptor.
563///
564/// The stable key is the durable allocation identity; physical placement is
565/// resolved by the runtime, not the schema model.
566/// `schema_version + schema_fingerprint_method_version + schema_fingerprint`
567/// is diagnostic metadata only.
568#[derive(Clone, Debug, Eq, PartialEq)]
569pub struct StableMemoryAllocation {
570    stable_key: String,
571    schema_metadata: StableMemoryAllocationMetadata,
572}
573
574impl StableMemoryAllocation {
575    /// Build an allocation descriptor without schema metadata.
576    #[must_use]
577    pub const fn without_schema_metadata(stable_key: String) -> Self {
578        Self::with_schema_metadata(stable_key, StableMemoryAllocationMetadata::absent())
579    }
580
581    /// Build an allocation descriptor with diagnostic schema metadata.
582    ///
583    /// The metadata must come from accepted schema/catalog authority. Generated
584    /// model fallback metadata is not an allocation metadata authority.
585    #[must_use]
586    pub const fn with_schema_metadata(
587        stable_key: String,
588        schema_metadata: StableMemoryAllocationMetadata,
589    ) -> Self {
590        Self {
591            stable_key,
592            schema_metadata,
593        }
594    }
595
596    /// Durable stable-memory key.
597    #[must_use]
598    pub const fn stable_key(&self) -> &str {
599        self.stable_key.as_str()
600    }
601
602    /// Diagnostic schema/catalog metadata.
603    #[must_use]
604    pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
605        &self.schema_metadata
606    }
607
608    /// Accepted schema/catalog version, when known.
609    #[must_use]
610    pub const fn schema_version(&self) -> Option<u32> {
611        self.schema_metadata.schema_version()
612    }
613
614    /// Accepted schema/catalog fingerprint method version, when known.
615    #[must_use]
616    pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
617        self.schema_metadata.schema_fingerprint_method_version()
618    }
619
620    /// Accepted schema/catalog fingerprint, when known.
621    #[must_use]
622    pub const fn schema_fingerprint(&self) -> Option<&str> {
623        self.schema_metadata.schema_fingerprint()
624    }
625
626    /// Compare durable allocation identity only.
627    ///
628    /// Schema metadata is intentionally ignored because metadata changes are
629    /// diagnostics, not memory replacement.
630    #[must_use]
631    pub fn same_identity_as(&self, other: &Self) -> bool {
632        self.stable_key == other.stable_key
633    }
634}
635
636#[must_use]
637pub fn stable_memory_key(memory_namespace: &str, allocation: &str, role: &str) -> String {
638    format!("icydb.{memory_namespace}.{allocation}.{role}.v1")
639}
640
641#[must_use]
642fn stable_store_memory_key(memory_namespace: &str, store_key: &str, role: &str) -> String {
643    // Explicit logical identity survives source-level renames and declaration order.
644    format!("icydb.{memory_namespace}.store.{store_key}.{role}.v1")
645}
646
647impl MacroNode for Store {
648    fn as_any(&self) -> &dyn std::any::Any {
649        self
650    }
651}
652
653impl ValidateNode for Store {
654    fn validate(&self) -> Result<(), ErrorTree> {
655        let mut errs = ErrorTree::new();
656
657        {
658            let schema = schema_read();
659
660            match schema.cast_node::<Canister>(self.canister()) {
661                Ok(canister) => match self.storage() {
662                    StoreStorage::Heap(_) => {}
663                    StoreStorage::Journaled(config) => {
664                        validate_journaled_memory_config(&mut errs, self, *config, canister);
665                    }
666                },
667                Err(e) => errs.add(e),
668            }
669        }
670
671        errs.result()
672    }
673}
674
675fn validate_journaled_memory_config(
676    errs: &mut ErrorTree,
677    store: &Store,
678    config: StoreJournaledMemoryConfig,
679    canister: &Canister,
680) {
681    validate_stable_key_segment(errs, "store key", config.key());
682    for allocation in [
683        store.stable_data_allocation(canister.memory_namespace()),
684        store.stable_index_allocation(canister.memory_namespace()),
685        store.stable_schema_allocation(canister.memory_namespace()),
686        store.journal_allocation(canister.memory_namespace()),
687    ] {
688        validate_stable_key(errs, "store allocation key", allocation.stable_key());
689    }
690}
691
692impl VisitableNode for Store {
693    fn route_key(&self) -> String {
694        self.def().path()
695    }
696
697    fn drive<V: Visitor>(&self, v: &mut V) {
698        self.def().accept(v);
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use crate::{
705        build::schema_write,
706        node::{Canister, SchemaNode},
707    };
708
709    use super::*;
710
711    fn insert_canister(path_module: &'static str, ident: &'static str) {
712        schema_write().insert_node(SchemaNode::Canister(Canister::new(
713            Def::new(path_module, ident),
714            "test_db",
715            None,
716        )));
717    }
718
719    #[test]
720    fn store_allocations_default_to_absent_schema_metadata() {
721        let store = Store::new_journaled(
722            Def::new("demo::rpg", "CharacterStore"),
723            "demo::rpg::Canister",
724            StoreJournaledMemoryConfig::new("store_110"),
725        );
726
727        for allocation in [
728            store.stable_data_allocation("demo_rpg"),
729            store.stable_index_allocation("demo_rpg"),
730            store.stable_schema_allocation("demo_rpg"),
731            store.journal_allocation("demo_rpg"),
732        ] {
733            assert_eq!(allocation.schema_version(), None);
734            assert_eq!(allocation.schema_fingerprint_method_version(), None);
735            assert_eq!(allocation.schema_fingerprint(), None);
736            assert_eq!(
737                allocation.schema_metadata(),
738                &StableMemoryAllocationMetadata::absent()
739            );
740        }
741    }
742
743    #[test]
744    fn allocation_metadata_is_role_specific_and_diagnostic_only() {
745        let store = Store::new_journaled(
746            Def::new("demo::rpg", "CharacterStore"),
747            "demo::rpg::Canister",
748            StoreJournaledMemoryConfig::new("store_110"),
749        );
750        let data = store.stable_data_allocation_with_schema_metadata(
751            "demo_rpg",
752            StableMemoryAllocationMetadata::from_accepted_schema_contract(
753                7,
754                1,
755                "data-row-layout".to_string(),
756            ),
757        );
758        let index = store.stable_index_allocation_with_schema_metadata(
759            "demo_rpg",
760            StableMemoryAllocationMetadata::from_accepted_schema_contract(
761                8,
762                1,
763                "index-catalog".to_string(),
764            ),
765        );
766        let schema = store.stable_schema_allocation_with_schema_metadata(
767            "demo_rpg",
768            StableMemoryAllocationMetadata::from_accepted_schema_contract(
769                10,
770                1,
771                "schema-catalog".to_string(),
772            ),
773        );
774        let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
775            "demo_rpg",
776            StableMemoryAllocationMetadata::from_accepted_schema_contract(
777                9,
778                1,
779                "data-row-layout-changed".to_string(),
780            ),
781        );
782
783        assert_eq!(data.schema_version(), Some(7));
784        assert_eq!(data.schema_fingerprint_method_version(), Some(1));
785        assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
786        assert_eq!(index.schema_version(), Some(8));
787        assert_eq!(index.schema_fingerprint_method_version(), Some(1));
788        assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
789        assert_eq!(schema.schema_version(), Some(10));
790        assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
791        assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
792        assert!(data.same_identity_as(&data_after_reconcile));
793        assert!(!data.same_identity_as(&index));
794        assert!(!data.same_identity_as(&schema));
795    }
796
797    #[test]
798    fn store_owns_explicit_heap_storage_config() {
799        insert_canister("store_heap_config", "Canister");
800        let store = Store::new_heap(
801            Def::new("store_heap_config", "Store"),
802            "store_heap_config::Canister",
803            StoreHeapConfig::new(),
804        );
805
806        assert!(store.is_heap_storage());
807        assert!(store.validate().is_ok());
808    }
809
810    #[test]
811    fn heap_store_storage_capabilities_describe_volatile_contract() {
812        let store = Store::new_heap(
813            Def::new("store_heap_capabilities", "Store"),
814            "store_heap_capabilities::Canister",
815            StoreHeapConfig::new(),
816        );
817        let capabilities = store.storage_capabilities();
818
819        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
820        assert_eq!(
821            capabilities.allocation_identity(),
822            AllocationIdentityCapability::Absent,
823        );
824        assert_eq!(capabilities.durability(), StoreDurability::Volatile);
825        assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
826        assert_eq!(
827            capabilities.commit_participation(),
828            CommitParticipation::LiveOnly,
829        );
830        assert_eq!(
831            capabilities.schema_metadata(),
832            SchemaMetadataCapability::LiveRebuiltMetadata,
833        );
834        assert_eq!(
835            capabilities.relation_source(),
836            RelationSourceCapability::LiveSource,
837        );
838        assert_eq!(
839            capabilities.relation_target(),
840            RelationTargetCapability::VolatileTarget,
841        );
842        assert_eq!(
843            capabilities.live_validation(),
844            LiveValidationCapability::Supported,
845        );
846        assert!(!capabilities.has_allocation_identity());
847        assert!(!capabilities.participates_in_durable_commit());
848        assert!(capabilities.is_volatile());
849    }
850
851    #[test]
852    fn store_owns_explicit_journaled_storage_config() {
853        insert_canister("store_journaled_config", "Canister");
854        let store = Store::new_journaled(
855            Def::new("store_journaled_config", "Store"),
856            "store_journaled_config::Canister",
857            StoreJournaledMemoryConfig::new("store_110"),
858        );
859
860        assert!(store.is_journaled_storage());
861        assert!(!store.is_heap_storage());
862        let journaled = store
863            .journaled_memory_config()
864            .expect("journaled model stores four-role config explicitly");
865        assert_eq!(journaled.key(), "store_110");
866        assert_eq!(store.stable_store_key(), "store_110");
867        assert!(store.validate().is_ok());
868    }
869
870    #[test]
871    fn store_keys_validate_segments_and_complete_role_key_lengths() {
872        let canister = Canister::new(Def::new("bounded_key", "Canister"), "app", None);
873        // The journal role is longest: prefix + store key + suffix = 27 + key bytes.
874        let admitted = "a".repeat(101);
875        let oversized = "a".repeat(102);
876        for (key, accepted) in [
877            ("transfers", true),
878            ("", false),
879            ("Transfers", false),
880            ("1store", false),
881            ("store.name", false),
882            (admitted.as_str(), true),
883            (oversized.as_str(), false),
884        ] {
885            let config =
886                StoreJournaledMemoryConfig::new(Box::leak(key.to_string().into_boxed_str()));
887            let store = Store::new_journaled(
888                Def::new("bounded_key", "Store"),
889                "bounded_key::Canister",
890                config,
891            );
892            let mut errors = ErrorTree::new();
893            validate_journaled_memory_config(&mut errors, &store, config, &canister);
894            assert_eq!(errors.result().is_ok(), accepted);
895        }
896    }
897
898    #[test]
899    fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
900        let store = Store::new_journaled(
901            Def::new("store_journaled_capabilities", "Store"),
902            "store_journaled_capabilities::Canister",
903            StoreJournaledMemoryConfig::new("store_110"),
904        );
905        let capabilities = store.storage_capabilities();
906
907        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
908        assert_eq!(
909            capabilities.allocation_identity(),
910            AllocationIdentityCapability::Present,
911        );
912        assert_eq!(capabilities.durability(), StoreDurability::Durable);
913        assert_eq!(
914            capabilities.recovery(),
915            StoreRecoveryCapability::StableBasePlusJournalReplay,
916        );
917        assert_eq!(
918            capabilities.commit_participation(),
919            CommitParticipation::Durable,
920        );
921        assert_eq!(
922            capabilities.schema_metadata(),
923            SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
924        );
925        assert_eq!(
926            capabilities.relation_source(),
927            RelationSourceCapability::DurableSource,
928        );
929        assert_eq!(
930            capabilities.relation_target(),
931            RelationTargetCapability::DurableTarget,
932        );
933        assert_eq!(
934            capabilities.live_validation(),
935            LiveValidationCapability::Supported,
936        );
937        assert!(capabilities.has_allocation_identity());
938        assert!(capabilities.participates_in_durable_commit());
939        assert!(!capabilities.is_volatile());
940    }
941
942    #[test]
943    fn journaled_store_allocations_use_role_named_stable_keys() {
944        let store = Store::new_journaled(
945            Def::new("demo::rpg", "CharacterStore"),
946            "demo::rpg::Canister",
947            StoreJournaledMemoryConfig::new("store_110"),
948        );
949
950        assert_eq!(
951            store.stable_data_allocation("demo_rpg").stable_key(),
952            "icydb.demo_rpg.store.store_110.data.v1",
953        );
954        assert_eq!(
955            store.stable_index_allocation("demo_rpg").stable_key(),
956            "icydb.demo_rpg.store.store_110.index.v1",
957        );
958        assert_eq!(
959            store.stable_schema_allocation("demo_rpg").stable_key(),
960            "icydb.demo_rpg.store.store_110.schema.v1",
961        );
962        assert_eq!(
963            store.journal_allocation("demo_rpg").stable_key(),
964            "icydb.demo_rpg.store.store_110.journal.v1",
965        );
966
967        // Rust names locate generated code; only the explicit key names memory.
968        let renamed = Store::new_journaled(
969            Def::new("renamed::module", "RenamedStore"),
970            "renamed::module::RenamedCanister",
971            StoreJournaledMemoryConfig::new("store_110"),
972        );
973        for (before, after) in [
974            (
975                store.stable_data_allocation("demo_rpg"),
976                renamed.stable_data_allocation("demo_rpg"),
977            ),
978            (
979                store.stable_index_allocation("demo_rpg"),
980                renamed.stable_index_allocation("demo_rpg"),
981            ),
982            (
983                store.stable_schema_allocation("demo_rpg"),
984                renamed.stable_schema_allocation("demo_rpg"),
985            ),
986            (
987                store.journal_allocation("demo_rpg"),
988                renamed.journal_allocation("demo_rpg"),
989            ),
990        ] {
991            assert_eq!(before.stable_key(), after.stable_key());
992        }
993    }
994
995    #[test]
996    fn storage_capabilities_are_not_allocation_identity() {
997        let store_a = Store::new_journaled(
998            Def::new("demo::rpg", "CharacterStore"),
999            "demo::rpg::Canister",
1000            StoreJournaledMemoryConfig::new("store_110"),
1001        );
1002        let store_b = Store::new_journaled(
1003            Def::new("demo::rpg", "InventoryStore"),
1004            "demo::rpg::Canister",
1005            StoreJournaledMemoryConfig::new("store_120"),
1006        );
1007
1008        assert_eq!(
1009            store_a.storage_capabilities(),
1010            store_b.storage_capabilities()
1011        );
1012        assert_ne!(
1013            store_a.stable_data_allocation("demo_rpg"),
1014            store_b.stable_data_allocation("demo_rpg"),
1015            "stable allocation identity must remain separate from capabilities",
1016        );
1017    }
1018
1019    #[test]
1020    fn capability_consumers_use_axes_not_storage_mode() {
1021        const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1022            match capabilities.commit_participation() {
1023                CommitParticipation::Durable => "durable",
1024                CommitParticipation::LiveOnly => "live-only",
1025            }
1026        }
1027
1028        let future_durable_heap_mode = StoreStorageCapabilities {
1029            storage_mode: StoreStorageMode::Heap,
1030            allocation_identity: AllocationIdentityCapability::Present,
1031            durability: StoreDurability::Durable,
1032            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1033            commit_participation: CommitParticipation::Durable,
1034            schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1035            relation_source: RelationSourceCapability::DurableSource,
1036            relation_target: RelationTargetCapability::DurableTarget,
1037            live_validation: LiveValidationCapability::Supported,
1038        };
1039
1040        assert_eq!(commit_label(future_durable_heap_mode), "durable");
1041        assert!(future_durable_heap_mode.participates_in_durable_commit());
1042        assert_eq!(
1043            future_durable_heap_mode.storage_mode(),
1044            StoreStorageMode::Heap,
1045            "the diagnostic storage mode must not drive commit policy",
1046        );
1047    }
1048}