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