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