Skip to main content

icydb_schema/node/
store.rs

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