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    data_memory_id: u8,
23    index_memory_id: u8,
24    schema_memory_id: u8,
25}
26
27impl Store {
28    #[must_use]
29    pub const fn new(
30        def: Def,
31        ident: &'static str,
32        store_name: &'static str,
33        canister: &'static str,
34        data_memory_id: u8,
35        index_memory_id: u8,
36        schema_memory_id: u8,
37    ) -> Self {
38        Self {
39            def,
40            ident,
41            name: store_name,
42            canister,
43            data_memory_id,
44            index_memory_id,
45            schema_memory_id,
46        }
47    }
48
49    #[must_use]
50    pub const fn def(&self) -> &Def {
51        &self.def
52    }
53
54    #[must_use]
55    pub const fn ident(&self) -> &'static str {
56        self.ident
57    }
58
59    #[must_use]
60    pub const fn store_name(&self) -> &'static str {
61        self.name
62    }
63
64    #[must_use]
65    pub const fn canister(&self) -> &'static str {
66        self.canister
67    }
68
69    #[must_use]
70    pub const fn data_memory_id(&self) -> u8 {
71        self.data_memory_id
72    }
73
74    #[must_use]
75    pub const fn index_memory_id(&self) -> u8 {
76        self.index_memory_id
77    }
78
79    #[must_use]
80    pub const fn schema_memory_id(&self) -> u8 {
81        self.schema_memory_id
82    }
83
84    #[must_use]
85    pub fn data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
86        self.allocation(memory_namespace, StoreMemoryRole::Data)
87    }
88
89    /// Build the data-memory allocation descriptor with accepted row-layout
90    /// schema metadata attached for diagnostics.
91    #[must_use]
92    pub fn data_allocation_with_schema_metadata(
93        &self,
94        memory_namespace: &str,
95        schema_metadata: StableMemoryAllocationMetadata,
96    ) -> StableMemoryAllocation {
97        self.allocation_with_schema_metadata(
98            memory_namespace,
99            StoreMemoryRole::Data,
100            schema_metadata,
101        )
102    }
103
104    #[must_use]
105    pub fn index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
106        self.allocation(memory_namespace, StoreMemoryRole::Index)
107    }
108
109    /// Build the index-memory allocation descriptor with accepted index-catalog
110    /// schema metadata attached for diagnostics.
111    #[must_use]
112    pub fn index_allocation_with_schema_metadata(
113        &self,
114        memory_namespace: &str,
115        schema_metadata: StableMemoryAllocationMetadata,
116    ) -> StableMemoryAllocation {
117        self.allocation_with_schema_metadata(
118            memory_namespace,
119            StoreMemoryRole::Index,
120            schema_metadata,
121        )
122    }
123
124    #[must_use]
125    pub fn schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
126        self.allocation(memory_namespace, StoreMemoryRole::Schema)
127    }
128
129    /// Build the schema-memory allocation descriptor with accepted catalog
130    /// schema metadata attached for diagnostics.
131    #[must_use]
132    pub fn schema_allocation_with_schema_metadata(
133        &self,
134        memory_namespace: &str,
135        schema_metadata: StableMemoryAllocationMetadata,
136    ) -> StableMemoryAllocation {
137        self.allocation_with_schema_metadata(
138            memory_namespace,
139            StoreMemoryRole::Schema,
140            schema_metadata,
141        )
142    }
143
144    #[must_use]
145    pub fn allocation(
146        &self,
147        memory_namespace: &str,
148        role: StoreMemoryRole,
149    ) -> StableMemoryAllocation {
150        let memory_id = match role {
151            StoreMemoryRole::Data => self.data_memory_id,
152            StoreMemoryRole::Index => self.index_memory_id,
153            StoreMemoryRole::Schema => self.schema_memory_id,
154        };
155
156        StableMemoryAllocation::without_schema_metadata(
157            memory_id,
158            stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
159        )
160    }
161
162    fn allocation_with_schema_metadata(
163        &self,
164        memory_namespace: &str,
165        role: StoreMemoryRole,
166        schema_metadata: StableMemoryAllocationMetadata,
167    ) -> StableMemoryAllocation {
168        let memory_id = match role {
169            StoreMemoryRole::Data => self.data_memory_id,
170            StoreMemoryRole::Index => self.index_memory_id,
171            StoreMemoryRole::Schema => self.schema_memory_id,
172        };
173
174        StableMemoryAllocation::with_schema_metadata(
175            memory_id,
176            stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
177            schema_metadata,
178        )
179    }
180}
181
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub enum StoreMemoryRole {
184    Data,
185    Index,
186    Schema,
187}
188
189impl StoreMemoryRole {
190    #[must_use]
191    pub const fn as_str(self) -> &'static str {
192        match self {
193            Self::Data => "data",
194            Self::Index => "index",
195            Self::Schema => "schema",
196        }
197    }
198}
199
200/// Diagnostic schema metadata associated with a stable-memory allocation.
201///
202/// This metadata does not participate in durable allocation identity. The
203/// durable identity remains `memory_id + stable_key`.
204#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct StableMemoryAllocationMetadata {
206    schema_version: Option<u32>,
207    schema_fingerprint: Option<String>,
208}
209
210impl StableMemoryAllocationMetadata {
211    const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
212        Self {
213            schema_version,
214            schema_fingerprint,
215        }
216    }
217
218    /// Build allocation metadata from an accepted schema/catalog authority.
219    #[must_use]
220    pub const fn from_accepted_schema_contract(
221        schema_version: u32,
222        schema_fingerprint: String,
223    ) -> Self {
224        Self::new(Some(schema_version), Some(schema_fingerprint))
225    }
226
227    /// Build absent allocation metadata for allocations with no accepted
228    /// schema/catalog authority.
229    #[must_use]
230    pub const fn absent() -> Self {
231        Self::new(None, None)
232    }
233
234    /// Accepted schema/catalog version, when known.
235    #[must_use]
236    pub const fn schema_version(&self) -> Option<u32> {
237        self.schema_version
238    }
239
240    /// Accepted schema/catalog fingerprint, when known.
241    #[must_use]
242    pub const fn schema_fingerprint(&self) -> Option<&str> {
243        match &self.schema_fingerprint {
244            Some(value) => Some(value.as_str()),
245            None => None,
246        }
247    }
248}
249
250/// Stable-memory allocation descriptor.
251///
252/// `memory_id + stable_key` is the durable allocation identity.
253/// `schema_version + schema_fingerprint` is diagnostic metadata only.
254#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct StableMemoryAllocation {
256    memory_id: u8,
257    stable_key: String,
258    schema_metadata: StableMemoryAllocationMetadata,
259}
260
261impl StableMemoryAllocation {
262    /// Build an allocation descriptor without schema metadata.
263    #[must_use]
264    pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
265        Self::with_schema_metadata(
266            memory_id,
267            stable_key,
268            StableMemoryAllocationMetadata::absent(),
269        )
270    }
271
272    /// Build an allocation descriptor with diagnostic schema metadata.
273    ///
274    /// The metadata must come from accepted schema/catalog authority. Generated
275    /// model fallback metadata is not an allocation metadata authority.
276    #[must_use]
277    pub const fn with_schema_metadata(
278        memory_id: u8,
279        stable_key: String,
280        schema_metadata: StableMemoryAllocationMetadata,
281    ) -> Self {
282        Self {
283            memory_id,
284            stable_key,
285            schema_metadata,
286        }
287    }
288
289    /// Stable-memory manager ID.
290    #[must_use]
291    pub const fn memory_id(&self) -> u8 {
292        self.memory_id
293    }
294
295    /// Durable stable-memory key.
296    #[must_use]
297    pub const fn stable_key(&self) -> &str {
298        self.stable_key.as_str()
299    }
300
301    /// Diagnostic schema/catalog metadata.
302    #[must_use]
303    pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
304        &self.schema_metadata
305    }
306
307    /// Accepted schema/catalog version, when known.
308    #[must_use]
309    pub const fn schema_version(&self) -> Option<u32> {
310        self.schema_metadata.schema_version()
311    }
312
313    /// Accepted schema/catalog fingerprint, when known.
314    #[must_use]
315    pub const fn schema_fingerprint(&self) -> Option<&str> {
316        self.schema_metadata.schema_fingerprint()
317    }
318
319    /// Compare durable allocation identity only.
320    ///
321    /// Schema metadata is intentionally ignored because metadata changes are
322    /// diagnostics, not memory replacement.
323    #[must_use]
324    pub fn same_identity_as(&self, other: &Self) -> bool {
325        self.memory_id == other.memory_id && self.stable_key == other.stable_key
326    }
327}
328
329#[must_use]
330pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
331    format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
332}
333
334impl MacroNode for Store {
335    fn as_any(&self) -> &dyn std::any::Any {
336        self
337    }
338}
339
340impl ValidateNode for Store {
341    fn validate(&self) -> Result<(), ErrorTree> {
342        let mut errs = ErrorTree::new();
343        let schema = schema_read();
344
345        match schema.cast_node::<Canister>(self.canister()) {
346            Ok(canister) => {
347                validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
348
349                // Validate data memory ID
350                validate_memory_id_in_range(
351                    &mut errs,
352                    "data_memory_id",
353                    self.data_memory_id(),
354                    canister.memory_min(),
355                    canister.memory_max(),
356                );
357                validate_app_memory_id(&mut errs, "data_memory_id", self.data_memory_id());
358                validate_memory_id_not_reserved(&mut errs, "data_memory_id", self.data_memory_id());
359                validate_stable_key(
360                    &mut errs,
361                    "data stable key",
362                    self.data_allocation(canister.memory_namespace())
363                        .stable_key(),
364                );
365
366                // Validate index memory ID
367                validate_memory_id_in_range(
368                    &mut errs,
369                    "index_memory_id",
370                    self.index_memory_id(),
371                    canister.memory_min(),
372                    canister.memory_max(),
373                );
374                validate_app_memory_id(&mut errs, "index_memory_id", self.index_memory_id());
375                validate_memory_id_not_reserved(
376                    &mut errs,
377                    "index_memory_id",
378                    self.index_memory_id(),
379                );
380                validate_stable_key(
381                    &mut errs,
382                    "index stable key",
383                    self.index_allocation(canister.memory_namespace())
384                        .stable_key(),
385                );
386
387                // Validate schema memory ID
388                validate_memory_id_in_range(
389                    &mut errs,
390                    "schema_memory_id",
391                    self.schema_memory_id(),
392                    canister.memory_min(),
393                    canister.memory_max(),
394                );
395                validate_app_memory_id(&mut errs, "schema_memory_id", self.schema_memory_id());
396                validate_memory_id_not_reserved(
397                    &mut errs,
398                    "schema_memory_id",
399                    self.schema_memory_id(),
400                );
401                validate_stable_key(
402                    &mut errs,
403                    "schema stable key",
404                    self.schema_allocation(canister.memory_namespace())
405                        .stable_key(),
406                );
407
408                // Ensure the per-store memories are distinct.
409                if self.data_memory_id() == self.index_memory_id() {
410                    err!(
411                        errs,
412                        "data_memory_id and index_memory_id must differ (both are {})",
413                        self.data_memory_id(),
414                    );
415                }
416                if self.data_memory_id() == self.schema_memory_id() {
417                    err!(
418                        errs,
419                        "data_memory_id and schema_memory_id must differ (both are {})",
420                        self.data_memory_id(),
421                    );
422                }
423                if self.index_memory_id() == self.schema_memory_id() {
424                    err!(
425                        errs,
426                        "index_memory_id and schema_memory_id must differ (both are {})",
427                        self.index_memory_id(),
428                    );
429                }
430            }
431            Err(e) => errs.add(e),
432        }
433
434        errs.result()
435    }
436}
437
438impl VisitableNode for Store {
439    fn route_key(&self) -> String {
440        self.def().path()
441    }
442
443    fn drive<V: Visitor>(&self, v: &mut V) {
444        self.def().accept(v);
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn store_stable_keys_use_durable_icydb_shape() {
454        let store = Store::new(
455            Def::new("demo::rpg", "CharacterStore"),
456            "CHARACTER_STORE",
457            "characters",
458            "demo::rpg::Canister",
459            110,
460            111,
461            112,
462        );
463
464        assert_eq!(
465            store.data_allocation("demo_rpg").stable_key(),
466            "icydb.demo_rpg.characters.data.v1",
467        );
468        assert_eq!(
469            store.index_allocation("demo_rpg").stable_key(),
470            "icydb.demo_rpg.characters.index.v1",
471        );
472        assert_eq!(
473            store.schema_allocation("demo_rpg").stable_key(),
474            "icydb.demo_rpg.characters.schema.v1",
475        );
476    }
477
478    #[test]
479    fn store_allocations_default_to_absent_schema_metadata() {
480        let store = Store::new(
481            Def::new("demo::rpg", "CharacterStore"),
482            "CHARACTER_STORE",
483            "characters",
484            "demo::rpg::Canister",
485            110,
486            111,
487            112,
488        );
489
490        for allocation in [
491            store.data_allocation("demo_rpg"),
492            store.index_allocation("demo_rpg"),
493            store.schema_allocation("demo_rpg"),
494        ] {
495            assert_eq!(allocation.schema_version(), None);
496            assert_eq!(allocation.schema_fingerprint(), None);
497            assert_eq!(
498                allocation.schema_metadata(),
499                &StableMemoryAllocationMetadata::absent()
500            );
501        }
502    }
503
504    #[test]
505    fn allocation_metadata_is_role_specific_and_diagnostic_only() {
506        let store = Store::new(
507            Def::new("demo::rpg", "CharacterStore"),
508            "CHARACTER_STORE",
509            "characters",
510            "demo::rpg::Canister",
511            110,
512            111,
513            112,
514        );
515        let data = store.data_allocation_with_schema_metadata(
516            "demo_rpg",
517            StableMemoryAllocationMetadata::from_accepted_schema_contract(
518                7,
519                "data-row-layout".to_string(),
520            ),
521        );
522        let index = store.index_allocation_with_schema_metadata(
523            "demo_rpg",
524            StableMemoryAllocationMetadata::from_accepted_schema_contract(
525                8,
526                "index-catalog".to_string(),
527            ),
528        );
529        let schema = store.schema_allocation_with_schema_metadata(
530            "demo_rpg",
531            StableMemoryAllocationMetadata::from_accepted_schema_contract(
532                10,
533                "schema-catalog".to_string(),
534            ),
535        );
536        let data_after_reconcile = store.data_allocation_with_schema_metadata(
537            "demo_rpg",
538            StableMemoryAllocationMetadata::from_accepted_schema_contract(
539                9,
540                "data-row-layout-v2".to_string(),
541            ),
542        );
543
544        assert_eq!(data.schema_version(), Some(7));
545        assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
546        assert_eq!(index.schema_version(), Some(8));
547        assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
548        assert_eq!(schema.schema_version(), Some(10));
549        assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
550        assert!(data.same_identity_as(&data_after_reconcile));
551        assert!(!data.same_identity_as(&index));
552        assert!(!data.same_identity_as(&schema));
553    }
554}