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    #[must_use]
381    pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
382        self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
383    }
384
385    /// Build the data-memory allocation descriptor with accepted row-layout
386    /// schema metadata attached for diagnostics.
387    #[must_use]
388    pub fn stable_data_allocation_with_schema_metadata(
389        &self,
390        memory_namespace: &str,
391        schema_metadata: StableMemoryAllocationMetadata,
392    ) -> StableMemoryAllocation {
393        self.stable_allocation_with_schema_metadata(
394            memory_namespace,
395            StoreMemoryRole::Data,
396            schema_metadata,
397        )
398    }
399
400    #[must_use]
401    pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
402        self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
403    }
404
405    /// Build the index-memory allocation descriptor with accepted index-catalog
406    /// schema metadata attached for diagnostics.
407    #[must_use]
408    pub fn stable_index_allocation_with_schema_metadata(
409        &self,
410        memory_namespace: &str,
411        schema_metadata: StableMemoryAllocationMetadata,
412    ) -> StableMemoryAllocation {
413        self.stable_allocation_with_schema_metadata(
414            memory_namespace,
415            StoreMemoryRole::Index,
416            schema_metadata,
417        )
418    }
419
420    #[must_use]
421    pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
422        self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
423    }
424
425    /// Build the journal-tail allocation descriptor for journaled stores.
426    #[must_use]
427    pub fn journal_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
428        StableMemoryAllocation::without_schema_metadata(stable_store_memory_key(
429            memory_namespace,
430            self.stable_store_key(),
431            "journal",
432        ))
433    }
434
435    /// Build the schema-memory allocation descriptor with accepted catalog
436    /// schema metadata attached for diagnostics.
437    #[must_use]
438    pub fn stable_schema_allocation_with_schema_metadata(
439        &self,
440        memory_namespace: &str,
441        schema_metadata: StableMemoryAllocationMetadata,
442    ) -> StableMemoryAllocation {
443        self.stable_allocation_with_schema_metadata(
444            memory_namespace,
445            StoreMemoryRole::Schema,
446            schema_metadata,
447        )
448    }
449
450    #[must_use]
451    pub fn stable_allocation(
452        &self,
453        memory_namespace: &str,
454        role: StoreMemoryRole,
455    ) -> StableMemoryAllocation {
456        StableMemoryAllocation::without_schema_metadata(stable_store_memory_key(
457            memory_namespace,
458            self.stable_store_key(),
459            role.as_str(),
460        ))
461    }
462
463    fn stable_allocation_with_schema_metadata(
464        &self,
465        memory_namespace: &str,
466        role: StoreMemoryRole,
467        schema_metadata: StableMemoryAllocationMetadata,
468    ) -> StableMemoryAllocation {
469        StableMemoryAllocation::with_schema_metadata(
470            stable_store_memory_key(memory_namespace, self.stable_store_key(), role.as_str()),
471            schema_metadata,
472        )
473    }
474}
475
476#[derive(Clone, Copy, Debug, Eq, PartialEq)]
477pub enum StoreMemoryRole {
478    Data,
479    Index,
480    Schema,
481}
482
483impl StoreMemoryRole {
484    #[must_use]
485    pub const fn as_str(self) -> &'static str {
486        match self {
487            Self::Data => "data",
488            Self::Index => "index",
489            Self::Schema => "schema",
490        }
491    }
492}
493
494/// Diagnostic schema metadata associated with a stable-memory allocation.
495///
496/// This metadata does not participate in durable allocation identity. The
497/// durable identity remains the permanent stable key.
498#[derive(Clone, Debug, Eq, PartialEq)]
499pub struct StableMemoryAllocationMetadata {
500    version: Option<u32>,
501    fingerprint_method_version: Option<u8>,
502    fingerprint: Option<String>,
503}
504
505impl StableMemoryAllocationMetadata {
506    const fn new(
507        schema_version: Option<u32>,
508        schema_fingerprint_method_version: Option<u8>,
509        schema_fingerprint: Option<String>,
510    ) -> Self {
511        Self {
512            version: schema_version,
513            fingerprint_method_version: schema_fingerprint_method_version,
514            fingerprint: schema_fingerprint,
515        }
516    }
517
518    /// Build allocation metadata from an accepted schema/catalog authority.
519    #[must_use]
520    pub const fn from_accepted_schema_contract(
521        schema_version: u32,
522        schema_fingerprint_method_version: u8,
523        schema_fingerprint: String,
524    ) -> Self {
525        Self::new(
526            Some(schema_version),
527            Some(schema_fingerprint_method_version),
528            Some(schema_fingerprint),
529        )
530    }
531
532    /// Build absent allocation metadata for allocations with no accepted
533    /// schema/catalog authority.
534    #[must_use]
535    pub const fn absent() -> Self {
536        Self::new(None, None, None)
537    }
538
539    /// Accepted schema/catalog version, when known.
540    #[must_use]
541    pub const fn schema_version(&self) -> Option<u32> {
542        self.version
543    }
544
545    /// Accepted schema/catalog fingerprint method version, when known.
546    #[must_use]
547    pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
548        self.fingerprint_method_version
549    }
550
551    /// Accepted schema/catalog fingerprint, when known.
552    #[must_use]
553    pub const fn schema_fingerprint(&self) -> Option<&str> {
554        match &self.fingerprint {
555            Some(value) => Some(value.as_str()),
556            None => None,
557        }
558    }
559}
560
561/// Stable-memory allocation descriptor.
562///
563/// The stable key is the durable allocation identity; physical placement is
564/// resolved by the runtime, not the schema model.
565/// `schema_version + schema_fingerprint_method_version + schema_fingerprint`
566/// is diagnostic metadata only.
567#[derive(Clone, Debug, Eq, PartialEq)]
568pub struct StableMemoryAllocation {
569    stable_key: String,
570    schema_metadata: StableMemoryAllocationMetadata,
571}
572
573impl StableMemoryAllocation {
574    /// Build an allocation descriptor without schema metadata.
575    #[must_use]
576    pub const fn without_schema_metadata(stable_key: String) -> Self {
577        Self::with_schema_metadata(stable_key, StableMemoryAllocationMetadata::absent())
578    }
579
580    /// Build an allocation descriptor with diagnostic schema metadata.
581    ///
582    /// The metadata must come from accepted schema/catalog authority. Generated
583    /// model fallback metadata is not an allocation metadata authority.
584    #[must_use]
585    pub const fn with_schema_metadata(
586        stable_key: String,
587        schema_metadata: StableMemoryAllocationMetadata,
588    ) -> Self {
589        Self {
590            stable_key,
591            schema_metadata,
592        }
593    }
594
595    /// Durable stable-memory key.
596    #[must_use]
597    pub const fn stable_key(&self) -> &str {
598        self.stable_key.as_str()
599    }
600
601    /// Diagnostic schema/catalog metadata.
602    #[must_use]
603    pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
604        &self.schema_metadata
605    }
606
607    /// Accepted schema/catalog version, when known.
608    #[must_use]
609    pub const fn schema_version(&self) -> Option<u32> {
610        self.schema_metadata.schema_version()
611    }
612
613    /// Accepted schema/catalog fingerprint method version, when known.
614    #[must_use]
615    pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
616        self.schema_metadata.schema_fingerprint_method_version()
617    }
618
619    /// Accepted schema/catalog fingerprint, when known.
620    #[must_use]
621    pub const fn schema_fingerprint(&self) -> Option<&str> {
622        self.schema_metadata.schema_fingerprint()
623    }
624
625    /// Compare durable allocation identity only.
626    ///
627    /// Schema metadata is intentionally ignored because metadata changes are
628    /// diagnostics, not memory replacement.
629    #[must_use]
630    pub fn same_identity_as(&self, other: &Self) -> bool {
631        self.stable_key == other.stable_key
632    }
633}
634
635#[must_use]
636pub fn stable_memory_key(memory_namespace: &str, allocation: &str, role: &str) -> String {
637    format!("icydb.{memory_namespace}.{allocation}.{role}.v1")
638}
639
640#[must_use]
641fn stable_store_memory_key(memory_namespace: &str, store_key: &str, role: &str) -> String {
642    // Explicit logical identity survives source-level renames and declaration order.
643    format!("icydb.{memory_namespace}.store.{store_key}.{role}.v1")
644}
645
646impl MacroNode for Store {
647    fn as_any(&self) -> &dyn std::any::Any {
648        self
649    }
650}
651
652impl ValidateNode for Store {
653    fn validate(&self) -> Result<(), ErrorTree> {
654        let mut errs = ErrorTree::new();
655
656        {
657            let schema = schema_read();
658
659            match schema.cast_node::<Canister>(self.canister()) {
660                Ok(canister) => match self.storage() {
661                    StoreStorage::Heap(_) => {}
662                    StoreStorage::Journaled(config) => {
663                        validate_journaled_memory_config(&mut errs, self, *config, canister);
664                    }
665                },
666                Err(e) => errs.add(e),
667            }
668        }
669
670        errs.result()
671    }
672}
673
674fn validate_journaled_memory_config(
675    errs: &mut ErrorTree,
676    store: &Store,
677    config: StoreJournaledMemoryConfig,
678    canister: &Canister,
679) {
680    validate_stable_key_segment(errs, "store key", config.key());
681    for allocation in [
682        store.stable_data_allocation(canister.memory_namespace()),
683        store.stable_index_allocation(canister.memory_namespace()),
684        store.stable_schema_allocation(canister.memory_namespace()),
685        store.journal_allocation(canister.memory_namespace()),
686    ] {
687        validate_stable_key(errs, "store allocation key", allocation.stable_key());
688    }
689}
690
691impl VisitableNode for Store {
692    fn route_key(&self) -> String {
693        self.def().path()
694    }
695
696    fn drive<V: Visitor>(&self, v: &mut V) {
697        self.def().accept(v);
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use crate::{
704        build::schema_write,
705        node::{Canister, SchemaNode},
706    };
707
708    use super::*;
709
710    fn insert_canister(path_module: &'static str, ident: &'static str) {
711        schema_write().insert_node(SchemaNode::Canister(Canister::new(
712            Def::new(path_module, ident),
713            "test_db",
714            None,
715        )));
716    }
717
718    #[test]
719    fn store_allocations_default_to_absent_schema_metadata() {
720        let store = Store::new_journaled(
721            Def::new("demo::rpg", "CharacterStore"),
722            "demo::rpg::Canister",
723            StoreJournaledMemoryConfig::new("store_110"),
724        );
725
726        for allocation in [
727            store.stable_data_allocation("demo_rpg"),
728            store.stable_index_allocation("demo_rpg"),
729            store.stable_schema_allocation("demo_rpg"),
730            store.journal_allocation("demo_rpg"),
731        ] {
732            assert_eq!(allocation.schema_version(), None);
733            assert_eq!(allocation.schema_fingerprint_method_version(), None);
734            assert_eq!(allocation.schema_fingerprint(), None);
735            assert_eq!(
736                allocation.schema_metadata(),
737                &StableMemoryAllocationMetadata::absent()
738            );
739        }
740    }
741
742    #[test]
743    fn allocation_metadata_is_role_specific_and_diagnostic_only() {
744        let store = Store::new_journaled(
745            Def::new("demo::rpg", "CharacterStore"),
746            "demo::rpg::Canister",
747            StoreJournaledMemoryConfig::new("store_110"),
748        );
749        let data = store.stable_data_allocation_with_schema_metadata(
750            "demo_rpg",
751            StableMemoryAllocationMetadata::from_accepted_schema_contract(
752                7,
753                1,
754                "data-row-layout".to_string(),
755            ),
756        );
757        let index = store.stable_index_allocation_with_schema_metadata(
758            "demo_rpg",
759            StableMemoryAllocationMetadata::from_accepted_schema_contract(
760                8,
761                1,
762                "index-catalog".to_string(),
763            ),
764        );
765        let schema = store.stable_schema_allocation_with_schema_metadata(
766            "demo_rpg",
767            StableMemoryAllocationMetadata::from_accepted_schema_contract(
768                10,
769                1,
770                "schema-catalog".to_string(),
771            ),
772        );
773        let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
774            "demo_rpg",
775            StableMemoryAllocationMetadata::from_accepted_schema_contract(
776                9,
777                1,
778                "data-row-layout-changed".to_string(),
779            ),
780        );
781
782        assert_eq!(data.schema_version(), Some(7));
783        assert_eq!(data.schema_fingerprint_method_version(), Some(1));
784        assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
785        assert_eq!(index.schema_version(), Some(8));
786        assert_eq!(index.schema_fingerprint_method_version(), Some(1));
787        assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
788        assert_eq!(schema.schema_version(), Some(10));
789        assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
790        assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
791        assert!(data.same_identity_as(&data_after_reconcile));
792        assert!(!data.same_identity_as(&index));
793        assert!(!data.same_identity_as(&schema));
794    }
795
796    #[test]
797    fn store_owns_explicit_heap_storage_config() {
798        insert_canister("store_heap_config", "Canister");
799        let store = Store::new_heap(
800            Def::new("store_heap_config", "Store"),
801            "store_heap_config::Canister",
802            StoreHeapConfig::new(),
803        );
804
805        assert!(store.is_heap_storage());
806        assert!(store.validate().is_ok());
807    }
808
809    #[test]
810    fn heap_store_storage_capabilities_describe_volatile_contract() {
811        let store = Store::new_heap(
812            Def::new("store_heap_capabilities", "Store"),
813            "store_heap_capabilities::Canister",
814            StoreHeapConfig::new(),
815        );
816        let capabilities = store.storage_capabilities();
817
818        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
819        assert_eq!(
820            capabilities.allocation_identity(),
821            AllocationIdentityCapability::Absent,
822        );
823        assert_eq!(capabilities.durability(), StoreDurability::Volatile);
824        assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
825        assert_eq!(
826            capabilities.commit_participation(),
827            CommitParticipation::LiveOnly,
828        );
829        assert_eq!(
830            capabilities.schema_metadata(),
831            SchemaMetadataCapability::LiveRebuiltMetadata,
832        );
833        assert_eq!(
834            capabilities.relation_source(),
835            RelationSourceCapability::LiveSource,
836        );
837        assert_eq!(
838            capabilities.relation_target(),
839            RelationTargetCapability::VolatileTarget,
840        );
841        assert_eq!(
842            capabilities.live_validation(),
843            LiveValidationCapability::Supported,
844        );
845        assert!(!capabilities.has_allocation_identity());
846        assert!(!capabilities.participates_in_durable_commit());
847        assert!(capabilities.is_volatile());
848    }
849
850    #[test]
851    fn store_owns_explicit_journaled_storage_config() {
852        insert_canister("store_journaled_config", "Canister");
853        let store = Store::new_journaled(
854            Def::new("store_journaled_config", "Store"),
855            "store_journaled_config::Canister",
856            StoreJournaledMemoryConfig::new("store_110"),
857        );
858
859        assert!(store.is_journaled_storage());
860        assert!(!store.is_heap_storage());
861        let journaled = store
862            .journaled_memory_config()
863            .expect("journaled model stores four-role config explicitly");
864        assert_eq!(journaled.key(), "store_110");
865        assert_eq!(store.stable_store_key(), "store_110");
866        assert!(store.validate().is_ok());
867    }
868
869    #[test]
870    fn store_keys_validate_segments_and_complete_role_key_lengths() {
871        let canister = Canister::new(Def::new("bounded_key", "Canister"), "app", None);
872        // The journal role is longest: prefix + store key + suffix = 27 + key bytes.
873        let admitted = "a".repeat(101);
874        let oversized = "a".repeat(102);
875        for (key, accepted) in [
876            ("transfers", true),
877            ("", false),
878            ("Transfers", false),
879            ("1store", false),
880            ("store.name", false),
881            (admitted.as_str(), true),
882            (oversized.as_str(), false),
883        ] {
884            let config =
885                StoreJournaledMemoryConfig::new(Box::leak(key.to_string().into_boxed_str()));
886            let store = Store::new_journaled(
887                Def::new("bounded_key", "Store"),
888                "bounded_key::Canister",
889                config,
890            );
891            let mut errors = ErrorTree::new();
892            validate_journaled_memory_config(&mut errors, &store, config, &canister);
893            assert_eq!(errors.result().is_ok(), accepted);
894        }
895    }
896
897    #[test]
898    fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
899        let store = Store::new_journaled(
900            Def::new("store_journaled_capabilities", "Store"),
901            "store_journaled_capabilities::Canister",
902            StoreJournaledMemoryConfig::new("store_110"),
903        );
904        let capabilities = store.storage_capabilities();
905
906        assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
907        assert_eq!(
908            capabilities.allocation_identity(),
909            AllocationIdentityCapability::Present,
910        );
911        assert_eq!(capabilities.durability(), StoreDurability::Durable);
912        assert_eq!(
913            capabilities.recovery(),
914            StoreRecoveryCapability::StableBasePlusJournalReplay,
915        );
916        assert_eq!(
917            capabilities.commit_participation(),
918            CommitParticipation::Durable,
919        );
920        assert_eq!(
921            capabilities.schema_metadata(),
922            SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
923        );
924        assert_eq!(
925            capabilities.relation_source(),
926            RelationSourceCapability::DurableSource,
927        );
928        assert_eq!(
929            capabilities.relation_target(),
930            RelationTargetCapability::DurableTarget,
931        );
932        assert_eq!(
933            capabilities.live_validation(),
934            LiveValidationCapability::Supported,
935        );
936        assert!(capabilities.has_allocation_identity());
937        assert!(capabilities.participates_in_durable_commit());
938        assert!(!capabilities.is_volatile());
939    }
940
941    #[test]
942    fn journaled_store_allocations_use_role_named_stable_keys() {
943        let store = Store::new_journaled(
944            Def::new("demo::rpg", "CharacterStore"),
945            "demo::rpg::Canister",
946            StoreJournaledMemoryConfig::new("store_110"),
947        );
948
949        assert_eq!(
950            store.stable_data_allocation("demo_rpg").stable_key(),
951            "icydb.demo_rpg.store.store_110.data.v1",
952        );
953        assert_eq!(
954            store.stable_index_allocation("demo_rpg").stable_key(),
955            "icydb.demo_rpg.store.store_110.index.v1",
956        );
957        assert_eq!(
958            store.stable_schema_allocation("demo_rpg").stable_key(),
959            "icydb.demo_rpg.store.store_110.schema.v1",
960        );
961        assert_eq!(
962            store.journal_allocation("demo_rpg").stable_key(),
963            "icydb.demo_rpg.store.store_110.journal.v1",
964        );
965
966        // Rust names locate generated code; only the explicit key names memory.
967        let renamed = Store::new_journaled(
968            Def::new("renamed::module", "RenamedStore"),
969            "renamed::module::RenamedCanister",
970            StoreJournaledMemoryConfig::new("store_110"),
971        );
972        for (before, after) in [
973            (
974                store.stable_data_allocation("demo_rpg"),
975                renamed.stable_data_allocation("demo_rpg"),
976            ),
977            (
978                store.stable_index_allocation("demo_rpg"),
979                renamed.stable_index_allocation("demo_rpg"),
980            ),
981            (
982                store.stable_schema_allocation("demo_rpg"),
983                renamed.stable_schema_allocation("demo_rpg"),
984            ),
985            (
986                store.journal_allocation("demo_rpg"),
987                renamed.journal_allocation("demo_rpg"),
988            ),
989        ] {
990            assert_eq!(before.stable_key(), after.stable_key());
991        }
992    }
993
994    #[test]
995    fn storage_capabilities_are_not_allocation_identity() {
996        let store_a = Store::new_journaled(
997            Def::new("demo::rpg", "CharacterStore"),
998            "demo::rpg::Canister",
999            StoreJournaledMemoryConfig::new("store_110"),
1000        );
1001        let store_b = Store::new_journaled(
1002            Def::new("demo::rpg", "InventoryStore"),
1003            "demo::rpg::Canister",
1004            StoreJournaledMemoryConfig::new("store_120"),
1005        );
1006
1007        assert_eq!(
1008            store_a.storage_capabilities(),
1009            store_b.storage_capabilities()
1010        );
1011        assert_ne!(
1012            store_a.stable_data_allocation("demo_rpg"),
1013            store_b.stable_data_allocation("demo_rpg"),
1014            "stable allocation identity must remain separate from capabilities",
1015        );
1016    }
1017
1018    #[test]
1019    fn capability_consumers_use_axes_not_storage_mode() {
1020        const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1021            match capabilities.commit_participation() {
1022                CommitParticipation::Durable => "durable",
1023                CommitParticipation::LiveOnly => "live-only",
1024            }
1025        }
1026
1027        let future_durable_heap_mode = StoreStorageCapabilities {
1028            storage_mode: StoreStorageMode::Heap,
1029            allocation_identity: AllocationIdentityCapability::Present,
1030            durability: StoreDurability::Durable,
1031            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1032            commit_participation: CommitParticipation::Durable,
1033            schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1034            relation_source: RelationSourceCapability::DurableSource,
1035            relation_target: RelationTargetCapability::DurableTarget,
1036            live_validation: LiveValidationCapability::Supported,
1037        };
1038
1039        assert_eq!(commit_label(future_durable_heap_mode), "durable");
1040        assert!(future_durable_heap_mode.participates_in_durable_commit());
1041        assert_eq!(
1042            future_durable_heap_mode.storage_mode(),
1043            StoreStorageMode::Heap,
1044            "the diagnostic storage mode must not drive commit policy",
1045        );
1046    }
1047}