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 stable IC BTreeMap memories that store:
11/// - primary entity data
12/// - all index data for that entity
13/// - persisted 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
52/// Heap storage configuration for one volatile store.
53#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
54pub struct StoreHeapConfig;
55
56impl StoreHeapConfig {
57    /// Build an empty heap storage configuration.
58    #[must_use]
59    pub const fn new() -> Self {
60        Self
61    }
62}
63
64/// Stable-memory IDs for the three durable roles owned by one store.
65#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
66pub struct StoreStableMemoryConfig {
67    data: u8,
68    index: u8,
69    schema: u8,
70}
71
72impl StoreStableMemoryConfig {
73    /// Build a stable-memory configuration from data, index, and schema memory
74    /// IDs.
75    #[must_use]
76    pub const fn new(data_memory_id: u8, index_memory_id: u8, schema_memory_id: u8) -> Self {
77        Self {
78            data: data_memory_id,
79            index: index_memory_id,
80            schema: schema_memory_id,
81        }
82    }
83
84    /// Data-store stable memory ID.
85    #[must_use]
86    pub const fn data_memory_id(self) -> u8 {
87        self.data
88    }
89
90    /// Index-store stable memory ID.
91    #[must_use]
92    pub const fn index_memory_id(self) -> u8 {
93        self.index
94    }
95
96    /// Schema-store stable memory ID.
97    #[must_use]
98    pub const fn schema_memory_id(self) -> u8 {
99        self.schema
100    }
101}
102
103impl Store {
104    /// Build a stable-memory-backed store declaration.
105    ///
106    /// This is the durable store constructor.
107    #[must_use]
108    pub const fn new_stable(
109        def: Def,
110        ident: &'static str,
111        store_name: &'static str,
112        canister: &'static str,
113        stable: StoreStableMemoryConfig,
114    ) -> Self {
115        Self {
116            def,
117            ident,
118            name: store_name,
119            canister,
120            storage: StoreStorage::Stable(stable),
121        }
122    }
123
124    /// Build a heap-backed volatile store declaration.
125    #[must_use]
126    pub const fn new_heap(
127        def: Def,
128        ident: &'static str,
129        store_name: &'static str,
130        canister: &'static str,
131        heap: StoreHeapConfig,
132    ) -> Self {
133        Self {
134            def,
135            ident,
136            name: store_name,
137            canister,
138            storage: StoreStorage::Heap(heap),
139        }
140    }
141
142    #[must_use]
143    pub const fn def(&self) -> &Def {
144        &self.def
145    }
146
147    #[must_use]
148    pub const fn ident(&self) -> &'static str {
149        self.ident
150    }
151
152    #[must_use]
153    pub const fn store_name(&self) -> &'static str {
154        self.name
155    }
156
157    #[must_use]
158    pub const fn canister(&self) -> &'static str {
159        self.canister
160    }
161
162    /// Borrow this store's storage configuration.
163    #[must_use]
164    pub const fn storage(&self) -> &StoreStorage {
165        &self.storage
166    }
167
168    /// Return whether this store is stable-memory-backed.
169    #[must_use]
170    pub const fn is_stable_storage(&self) -> bool {
171        matches!(self.storage, StoreStorage::Stable(_))
172    }
173
174    /// Return whether this store is heap-backed and volatile.
175    #[must_use]
176    pub const fn is_heap_storage(&self) -> bool {
177        matches!(self.storage, StoreStorage::Heap(_))
178    }
179
180    /// Borrow stable-memory IDs when this store uses stable storage.
181    #[must_use]
182    pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
183        self.storage.stable_memory_config()
184    }
185
186    #[must_use]
187    pub const fn stable_data_memory_id(&self) -> u8 {
188        match self.storage {
189            StoreStorage::Stable(config) => config.data_memory_id(),
190            StoreStorage::Heap(_) => panic!("heap stores do not have a stable data memory id"),
191        }
192    }
193
194    #[must_use]
195    pub const fn stable_index_memory_id(&self) -> u8 {
196        match self.storage {
197            StoreStorage::Stable(config) => config.index_memory_id(),
198            StoreStorage::Heap(_) => panic!("heap stores do not have a stable index memory id"),
199        }
200    }
201
202    #[must_use]
203    pub const fn stable_schema_memory_id(&self) -> u8 {
204        match self.storage {
205            StoreStorage::Stable(config) => config.schema_memory_id(),
206            StoreStorage::Heap(_) => panic!("heap stores do not have a stable schema memory id"),
207        }
208    }
209
210    #[must_use]
211    pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
212        self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
213    }
214
215    /// Build the data-memory allocation descriptor with accepted row-layout
216    /// schema metadata attached for diagnostics.
217    #[must_use]
218    pub fn stable_data_allocation_with_schema_metadata(
219        &self,
220        memory_namespace: &str,
221        schema_metadata: StableMemoryAllocationMetadata,
222    ) -> StableMemoryAllocation {
223        self.stable_allocation_with_schema_metadata(
224            memory_namespace,
225            StoreMemoryRole::Data,
226            schema_metadata,
227        )
228    }
229
230    #[must_use]
231    pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
232        self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
233    }
234
235    /// Build the index-memory allocation descriptor with accepted index-catalog
236    /// schema metadata attached for diagnostics.
237    #[must_use]
238    pub fn stable_index_allocation_with_schema_metadata(
239        &self,
240        memory_namespace: &str,
241        schema_metadata: StableMemoryAllocationMetadata,
242    ) -> StableMemoryAllocation {
243        self.stable_allocation_with_schema_metadata(
244            memory_namespace,
245            StoreMemoryRole::Index,
246            schema_metadata,
247        )
248    }
249
250    #[must_use]
251    pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
252        self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
253    }
254
255    /// Build the schema-memory allocation descriptor with accepted catalog
256    /// schema metadata attached for diagnostics.
257    #[must_use]
258    pub fn stable_schema_allocation_with_schema_metadata(
259        &self,
260        memory_namespace: &str,
261        schema_metadata: StableMemoryAllocationMetadata,
262    ) -> StableMemoryAllocation {
263        self.stable_allocation_with_schema_metadata(
264            memory_namespace,
265            StoreMemoryRole::Schema,
266            schema_metadata,
267        )
268    }
269
270    #[must_use]
271    pub fn stable_allocation(
272        &self,
273        memory_namespace: &str,
274        role: StoreMemoryRole,
275    ) -> StableMemoryAllocation {
276        let memory_id = match role {
277            StoreMemoryRole::Data => self.stable_data_memory_id(),
278            StoreMemoryRole::Index => self.stable_index_memory_id(),
279            StoreMemoryRole::Schema => self.stable_schema_memory_id(),
280        };
281
282        StableMemoryAllocation::without_schema_metadata(
283            memory_id,
284            stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
285        )
286    }
287
288    fn stable_allocation_with_schema_metadata(
289        &self,
290        memory_namespace: &str,
291        role: StoreMemoryRole,
292        schema_metadata: StableMemoryAllocationMetadata,
293    ) -> StableMemoryAllocation {
294        let memory_id = match role {
295            StoreMemoryRole::Data => self.stable_data_memory_id(),
296            StoreMemoryRole::Index => self.stable_index_memory_id(),
297            StoreMemoryRole::Schema => self.stable_schema_memory_id(),
298        };
299
300        StableMemoryAllocation::with_schema_metadata(
301            memory_id,
302            stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
303            schema_metadata,
304        )
305    }
306}
307
308#[derive(Clone, Copy, Debug, Eq, PartialEq)]
309pub enum StoreMemoryRole {
310    Data,
311    Index,
312    Schema,
313}
314
315impl StoreMemoryRole {
316    #[must_use]
317    pub const fn as_str(self) -> &'static str {
318        match self {
319            Self::Data => "data",
320            Self::Index => "index",
321            Self::Schema => "schema",
322        }
323    }
324}
325
326/// Diagnostic schema metadata associated with a stable-memory allocation.
327///
328/// This metadata does not participate in durable allocation identity. The
329/// durable identity remains `memory_id + stable_key`.
330#[derive(Clone, Debug, Eq, PartialEq)]
331pub struct StableMemoryAllocationMetadata {
332    schema_version: Option<u32>,
333    schema_fingerprint: Option<String>,
334}
335
336impl StableMemoryAllocationMetadata {
337    const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
338        Self {
339            schema_version,
340            schema_fingerprint,
341        }
342    }
343
344    /// Build allocation metadata from an accepted schema/catalog authority.
345    #[must_use]
346    pub const fn from_accepted_schema_contract(
347        schema_version: u32,
348        schema_fingerprint: String,
349    ) -> Self {
350        Self::new(Some(schema_version), Some(schema_fingerprint))
351    }
352
353    /// Build absent allocation metadata for allocations with no accepted
354    /// schema/catalog authority.
355    #[must_use]
356    pub const fn absent() -> Self {
357        Self::new(None, None)
358    }
359
360    /// Accepted schema/catalog version, when known.
361    #[must_use]
362    pub const fn schema_version(&self) -> Option<u32> {
363        self.schema_version
364    }
365
366    /// Accepted schema/catalog fingerprint, when known.
367    #[must_use]
368    pub const fn schema_fingerprint(&self) -> Option<&str> {
369        match &self.schema_fingerprint {
370            Some(value) => Some(value.as_str()),
371            None => None,
372        }
373    }
374}
375
376/// Stable-memory allocation descriptor.
377///
378/// `memory_id + stable_key` is the durable allocation identity.
379/// `schema_version + schema_fingerprint` is diagnostic metadata only.
380#[derive(Clone, Debug, Eq, PartialEq)]
381pub struct StableMemoryAllocation {
382    memory_id: u8,
383    stable_key: String,
384    schema_metadata: StableMemoryAllocationMetadata,
385}
386
387impl StableMemoryAllocation {
388    /// Build an allocation descriptor without schema metadata.
389    #[must_use]
390    pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
391        Self::with_schema_metadata(
392            memory_id,
393            stable_key,
394            StableMemoryAllocationMetadata::absent(),
395        )
396    }
397
398    /// Build an allocation descriptor with diagnostic schema metadata.
399    ///
400    /// The metadata must come from accepted schema/catalog authority. Generated
401    /// model fallback metadata is not an allocation metadata authority.
402    #[must_use]
403    pub const fn with_schema_metadata(
404        memory_id: u8,
405        stable_key: String,
406        schema_metadata: StableMemoryAllocationMetadata,
407    ) -> Self {
408        Self {
409            memory_id,
410            stable_key,
411            schema_metadata,
412        }
413    }
414
415    /// Stable-memory manager ID.
416    #[must_use]
417    pub const fn memory_id(&self) -> u8 {
418        self.memory_id
419    }
420
421    /// Durable stable-memory key.
422    #[must_use]
423    pub const fn stable_key(&self) -> &str {
424        self.stable_key.as_str()
425    }
426
427    /// Diagnostic schema/catalog metadata.
428    #[must_use]
429    pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
430        &self.schema_metadata
431    }
432
433    /// Accepted schema/catalog version, when known.
434    #[must_use]
435    pub const fn schema_version(&self) -> Option<u32> {
436        self.schema_metadata.schema_version()
437    }
438
439    /// Accepted schema/catalog fingerprint, when known.
440    #[must_use]
441    pub const fn schema_fingerprint(&self) -> Option<&str> {
442        self.schema_metadata.schema_fingerprint()
443    }
444
445    /// Compare durable allocation identity only.
446    ///
447    /// Schema metadata is intentionally ignored because metadata changes are
448    /// diagnostics, not memory replacement.
449    #[must_use]
450    pub fn same_identity_as(&self, other: &Self) -> bool {
451        self.memory_id == other.memory_id && self.stable_key == other.stable_key
452    }
453}
454
455#[must_use]
456pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
457    format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
458}
459
460impl MacroNode for Store {
461    fn as_any(&self) -> &dyn std::any::Any {
462        self
463    }
464}
465
466impl ValidateNode for Store {
467    fn validate(&self) -> Result<(), ErrorTree> {
468        let mut errs = ErrorTree::new();
469        let schema = schema_read();
470
471        match schema.cast_node::<Canister>(self.canister()) {
472            Ok(canister) => {
473                validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
474                match self.storage() {
475                    StoreStorage::Stable(config) => {
476                        validate_stable_memory_config(&mut errs, self, *config, canister);
477                    }
478                    StoreStorage::Heap(_) => {}
479                }
480            }
481            Err(e) => errs.add(e),
482        }
483
484        errs.result()
485    }
486}
487
488fn validate_stable_memory_config(
489    errs: &mut ErrorTree,
490    store: &Store,
491    config: StoreStableMemoryConfig,
492    canister: &Canister,
493) {
494    validate_stable_memory_role(
495        errs,
496        "data_memory_id",
497        "data stable key",
498        config.data_memory_id(),
499        store
500            .stable_data_allocation(canister.memory_namespace())
501            .stable_key(),
502        canister,
503    );
504    validate_stable_memory_role(
505        errs,
506        "index_memory_id",
507        "index stable key",
508        config.index_memory_id(),
509        store
510            .stable_index_allocation(canister.memory_namespace())
511            .stable_key(),
512        canister,
513    );
514    validate_stable_memory_role(
515        errs,
516        "schema_memory_id",
517        "schema stable key",
518        config.schema_memory_id(),
519        store
520            .stable_schema_allocation(canister.memory_namespace())
521            .stable_key(),
522        canister,
523    );
524
525    if config.data_memory_id() == config.index_memory_id() {
526        err!(
527            errs,
528            "data_memory_id and index_memory_id must differ (both are {})",
529            config.data_memory_id(),
530        );
531    }
532    if config.data_memory_id() == config.schema_memory_id() {
533        err!(
534            errs,
535            "data_memory_id and schema_memory_id must differ (both are {})",
536            config.data_memory_id(),
537        );
538    }
539    if config.index_memory_id() == config.schema_memory_id() {
540        err!(
541            errs,
542            "index_memory_id and schema_memory_id must differ (both are {})",
543            config.index_memory_id(),
544        );
545    }
546}
547
548fn validate_stable_memory_role(
549    errs: &mut ErrorTree,
550    memory_label: &str,
551    stable_key_label: &str,
552    memory_id: u8,
553    stable_key: &str,
554    canister: &Canister,
555) {
556    validate_memory_id_in_range(
557        errs,
558        memory_label,
559        memory_id,
560        canister.memory_min(),
561        canister.memory_max(),
562    );
563    validate_app_memory_id(errs, memory_label, memory_id);
564    validate_memory_id_not_reserved(errs, memory_label, memory_id);
565    validate_stable_key(errs, stable_key_label, stable_key);
566}
567
568impl VisitableNode for Store {
569    fn route_key(&self) -> String {
570        self.def().path()
571    }
572
573    fn drive<V: Visitor>(&self, v: &mut V) {
574        self.def().accept(v);
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use crate::{
581        build::schema_write,
582        node::{Canister, SchemaNode},
583    };
584
585    use super::*;
586
587    fn insert_canister(path_module: &'static str, ident: &'static str) {
588        schema_write().insert_node(SchemaNode::Canister(Canister::new(
589            Def::new(path_module, ident),
590            "test_db",
591            100,
592            254,
593            254,
594        )));
595    }
596
597    #[test]
598    fn store_stable_keys_use_durable_icydb_shape() {
599        let store = Store::new_stable(
600            Def::new("demo::rpg", "CharacterStore"),
601            "CHARACTER_STORE",
602            "characters",
603            "demo::rpg::Canister",
604            StoreStableMemoryConfig::new(110, 111, 112),
605        );
606
607        assert_eq!(
608            store.stable_data_allocation("demo_rpg").stable_key(),
609            "icydb.demo_rpg.characters.data.v1",
610        );
611        assert_eq!(
612            store.stable_index_allocation("demo_rpg").stable_key(),
613            "icydb.demo_rpg.characters.index.v1",
614        );
615        assert_eq!(
616            store.stable_schema_allocation("demo_rpg").stable_key(),
617            "icydb.demo_rpg.characters.schema.v1",
618        );
619    }
620
621    #[test]
622    fn store_allocations_default_to_absent_schema_metadata() {
623        let store = Store::new_stable(
624            Def::new("demo::rpg", "CharacterStore"),
625            "CHARACTER_STORE",
626            "characters",
627            "demo::rpg::Canister",
628            StoreStableMemoryConfig::new(110, 111, 112),
629        );
630
631        for allocation in [
632            store.stable_data_allocation("demo_rpg"),
633            store.stable_index_allocation("demo_rpg"),
634            store.stable_schema_allocation("demo_rpg"),
635        ] {
636            assert_eq!(allocation.schema_version(), None);
637            assert_eq!(allocation.schema_fingerprint(), None);
638            assert_eq!(
639                allocation.schema_metadata(),
640                &StableMemoryAllocationMetadata::absent()
641            );
642        }
643    }
644
645    #[test]
646    fn allocation_metadata_is_role_specific_and_diagnostic_only() {
647        let store = Store::new_stable(
648            Def::new("demo::rpg", "CharacterStore"),
649            "CHARACTER_STORE",
650            "characters",
651            "demo::rpg::Canister",
652            StoreStableMemoryConfig::new(110, 111, 112),
653        );
654        let data = store.stable_data_allocation_with_schema_metadata(
655            "demo_rpg",
656            StableMemoryAllocationMetadata::from_accepted_schema_contract(
657                7,
658                "data-row-layout".to_string(),
659            ),
660        );
661        let index = store.stable_index_allocation_with_schema_metadata(
662            "demo_rpg",
663            StableMemoryAllocationMetadata::from_accepted_schema_contract(
664                8,
665                "index-catalog".to_string(),
666            ),
667        );
668        let schema = store.stable_schema_allocation_with_schema_metadata(
669            "demo_rpg",
670            StableMemoryAllocationMetadata::from_accepted_schema_contract(
671                10,
672                "schema-catalog".to_string(),
673            ),
674        );
675        let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
676            "demo_rpg",
677            StableMemoryAllocationMetadata::from_accepted_schema_contract(
678                9,
679                "data-row-layout-v2".to_string(),
680            ),
681        );
682
683        assert_eq!(data.schema_version(), Some(7));
684        assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
685        assert_eq!(index.schema_version(), Some(8));
686        assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
687        assert_eq!(schema.schema_version(), Some(10));
688        assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
689        assert!(data.same_identity_as(&data_after_reconcile));
690        assert!(!data.same_identity_as(&index));
691        assert!(!data.same_identity_as(&schema));
692    }
693
694    #[test]
695    fn store_owns_explicit_stable_storage_config() {
696        let store = Store::new_stable(
697            Def::new("demo::rpg", "CharacterStore"),
698            "CHARACTER_STORE",
699            "characters",
700            "demo::rpg::Canister",
701            StoreStableMemoryConfig::new(110, 111, 112),
702        );
703
704        assert!(store.is_stable_storage());
705        assert!(store.storage().stable_memory_config().is_some());
706        let stable = store
707            .stable_memory_config()
708            .expect("0.167 model stores stable config explicitly");
709
710        assert_eq!(stable.data_memory_id(), 110);
711        assert_eq!(stable.index_memory_id(), 111);
712        assert_eq!(stable.schema_memory_id(), 112);
713        assert_eq!(store.stable_data_memory_id(), 110);
714        assert_eq!(store.stable_index_memory_id(), 111);
715        assert_eq!(store.stable_schema_memory_id(), 112);
716    }
717
718    #[test]
719    fn store_owns_explicit_heap_storage_config() {
720        insert_canister("store_heap_config", "Canister");
721        let store = Store::new_heap(
722            Def::new("store_heap_config", "Store"),
723            "STORE",
724            "heap_store",
725            "store_heap_config::Canister",
726            StoreHeapConfig::new(),
727        );
728
729        assert!(store.is_heap_storage());
730        assert!(!store.is_stable_storage());
731        assert!(store.stable_memory_config().is_none());
732        assert!(store.validate().is_ok());
733    }
734
735    #[test]
736    fn store_stable_storage_config_rejects_duplicate_role_memory_ids() {
737        insert_canister("store_duplicate_role_memory_ids", "Canister");
738        let store = Store::new_stable(
739            Def::new("store_duplicate_role_memory_ids", "Store"),
740            "STORE",
741            "duplicate_role_memory_ids",
742            "store_duplicate_role_memory_ids::Canister",
743            StoreStableMemoryConfig::new(110, 110, 112),
744        );
745
746        let err = store
747            .validate()
748            .expect_err("duplicate store role memory IDs must fail validation");
749        let rendered = err.to_string();
750
751        assert!(
752            rendered.contains("data_memory_id and index_memory_id must differ"),
753            "expected duplicate role memory-id error, got: {rendered}"
754        );
755    }
756}