Skip to main content

icydb_schema/node/
canister.rs

1use crate::node::{
2    stable_memory_key, validate_app_memory_id, validate_memory_id_in_range,
3    validate_memory_id_not_reserved, validate_stable_key, validate_stable_key_segment,
4};
5use crate::prelude::*;
6use std::collections::BTreeMap;
7
8///
9/// Canister
10///
11
12#[derive(CandidType, Clone, Debug, Serialize)]
13pub struct Canister {
14    def: Def,
15    memory_namespace: &'static str,
16    memory_min: u8,
17    memory_max: u8,
18    commit_memory_id: u8,
19}
20
21impl Canister {
22    #[must_use]
23    pub const fn new(
24        def: Def,
25        memory_namespace: &'static str,
26        memory_min: u8,
27        memory_max: u8,
28        commit_memory_id: u8,
29    ) -> Self {
30        Self {
31            def,
32            memory_namespace,
33            memory_min,
34            memory_max,
35            commit_memory_id,
36        }
37    }
38
39    #[must_use]
40    pub const fn def(&self) -> &Def {
41        &self.def
42    }
43
44    #[must_use]
45    pub const fn memory_namespace(&self) -> &'static str {
46        self.memory_namespace
47    }
48
49    #[must_use]
50    pub const fn memory_min(&self) -> u8 {
51        self.memory_min
52    }
53
54    #[must_use]
55    pub const fn memory_max(&self) -> u8 {
56        self.memory_max
57    }
58
59    #[must_use]
60    pub const fn commit_memory_id(&self) -> u8 {
61        self.commit_memory_id
62    }
63
64    #[must_use]
65    pub fn commit_stable_key(&self) -> String {
66        stable_memory_key(self.memory_namespace(), "commit", "control")
67    }
68}
69
70impl MacroNode for Canister {
71    fn as_any(&self) -> &dyn std::any::Any {
72        self
73    }
74}
75
76impl ValidateNode for Canister {
77    fn validate(&self) -> Result<(), ErrorTree> {
78        let mut errs = ErrorTree::new();
79        let schema = schema_read();
80
81        let canister_path = self.def().path();
82        let mut seen_ids = BTreeMap::<u8, (String, String)>::new();
83        let mut seen_keys = BTreeMap::<String, (u8, String)>::new();
84
85        validate_stable_key_segment(
86            &mut errs,
87            "canister memory_namespace",
88            self.memory_namespace(),
89        );
90
91        validate_memory_id_in_range(
92            &mut errs,
93            "commit_memory_id",
94            self.commit_memory_id(),
95            self.memory_min(),
96            self.memory_max(),
97        );
98        validate_app_memory_id(&mut errs, "commit_memory_id", self.commit_memory_id());
99        validate_memory_id_not_reserved(&mut errs, "commit_memory_id", self.commit_memory_id());
100        validate_stable_key(&mut errs, "commit stable key", &self.commit_stable_key());
101
102        assert_unique_memory_allocation(
103            self.commit_memory_id(),
104            self.commit_stable_key(),
105            format!("Canister `{}`.commit_memory", self.def().path()),
106            &canister_path,
107            &mut seen_ids,
108            &mut seen_keys,
109            &mut errs,
110        );
111
112        // Check all Store nodes for this canister
113        for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
114            match store.storage() {
115                StoreStorage::Stable(_) => {
116                    assert_unique_memory_allocation(
117                        store
118                            .stable_data_allocation(self.memory_namespace())
119                            .memory_id(),
120                        store
121                            .stable_data_allocation(self.memory_namespace())
122                            .stable_key()
123                            .to_string(),
124                        format!("Store `{path}`.data_memory"),
125                        &canister_path,
126                        &mut seen_ids,
127                        &mut seen_keys,
128                        &mut errs,
129                    );
130
131                    assert_unique_memory_allocation(
132                        store
133                            .stable_index_allocation(self.memory_namespace())
134                            .memory_id(),
135                        store
136                            .stable_index_allocation(self.memory_namespace())
137                            .stable_key()
138                            .to_string(),
139                        format!("Store `{path}`.index_memory"),
140                        &canister_path,
141                        &mut seen_ids,
142                        &mut seen_keys,
143                        &mut errs,
144                    );
145
146                    assert_unique_memory_allocation(
147                        store
148                            .stable_schema_allocation(self.memory_namespace())
149                            .memory_id(),
150                        store
151                            .stable_schema_allocation(self.memory_namespace())
152                            .stable_key()
153                            .to_string(),
154                        format!("Store `{path}`.schema_memory"),
155                        &canister_path,
156                        &mut seen_ids,
157                        &mut seen_keys,
158                        &mut errs,
159                    );
160                }
161                StoreStorage::Heap(_) => {}
162            }
163        }
164
165        errs.result()
166    }
167}
168
169fn assert_unique_memory_allocation(
170    memory_id: u8,
171    stable_key: String,
172    slot: String,
173    canister_path: &str,
174    seen_ids: &mut BTreeMap<u8, (String, String)>,
175    seen_keys: &mut BTreeMap<String, (u8, String)>,
176    errs: &mut ErrorTree,
177) {
178    if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
179        err!(
180            errs,
181            "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
182            memory_id,
183            canister_path,
184            existing_slot,
185            existing_key,
186            slot,
187            stable_key,
188        );
189    } else {
190        seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
191    }
192
193    if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
194        err!(
195            errs,
196            "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
197            stable_key,
198            canister_path,
199            existing_slot,
200            existing_id,
201            slot,
202            memory_id,
203        );
204    } else {
205        seen_keys.insert(stable_key, (memory_id, slot));
206    }
207}
208
209impl VisitableNode for Canister {
210    fn route_key(&self) -> String {
211        self.def().path()
212    }
213}
214
215//
216// TESTS
217//
218
219#[cfg(test)]
220mod tests {
221    use crate::build::schema_write;
222
223    use super::*;
224
225    fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
226        let canister = Canister::new(Def::new(path_module, ident), "test_db", 100, 254, 254);
227        schema_write().insert_node(SchemaNode::Canister(canister.clone()));
228
229        canister
230    }
231
232    fn insert_store(
233        path_module: &'static str,
234        ident: &'static str,
235        store_name: &'static str,
236        canister_path: &'static str,
237        data_memory_id: u8,
238        index_memory_id: u8,
239        schema_memory_id: u8,
240    ) {
241        schema_write().insert_node(SchemaNode::Store(Store::new_stable(
242            Def::new(path_module, ident),
243            ident,
244            store_name,
245            canister_path,
246            StoreStableMemoryConfig::new(data_memory_id, index_memory_id, schema_memory_id),
247        )));
248    }
249
250    #[test]
251    fn validate_rejects_memory_id_collision_between_stores() {
252        let canister = insert_canister("schema_store_collision", "Canister");
253        let canister_path = "schema_store_collision::Canister";
254
255        insert_store(
256            "schema_store_collision",
257            "StoreA",
258            "store_a",
259            canister_path,
260            110,
261            111,
262            112,
263        );
264        insert_store(
265            "schema_store_collision",
266            "StoreB",
267            "store_b",
268            canister_path,
269            113,
270            110,
271            114,
272        ); // collision
273
274        let err = canister
275            .validate()
276            .expect_err("memory-id collision must fail");
277
278        let rendered = err.to_string();
279        assert!(
280            rendered.contains("duplicate memory_id `110`"),
281            "expected duplicate memory-id error, got: {rendered}"
282        );
283    }
284
285    #[test]
286    fn validate_accepts_unique_memory_ids() {
287        let canister = insert_canister("schema_store_unique", "Canister");
288        let canister_path = "schema_store_unique::Canister";
289
290        insert_store(
291            "schema_store_unique",
292            "StoreA",
293            "store_a",
294            canister_path,
295            130,
296            131,
297            132,
298        );
299        insert_store(
300            "schema_store_unique",
301            "StoreB",
302            "store_b",
303            canister_path,
304            133,
305            134,
306            135,
307        );
308
309        canister.validate().expect("unique memory IDs should pass");
310    }
311
312    #[test]
313    fn validate_rejects_reserved_commit_memory_id() {
314        let canister = Canister::new(
315            Def::new("schema_reserved_commit", "Canister"),
316            "test_db",
317            100,
318            254,
319            255,
320        );
321        schema_write().insert_node(SchemaNode::Canister(canister.clone()));
322
323        let err = canister
324            .validate()
325            .expect_err("reserved commit memory id must fail");
326
327        let rendered = err.to_string();
328        assert!(
329            rendered.contains("reserved for stable-structures internals"),
330            "expected reserved-id error, got: {rendered}"
331        );
332    }
333
334    #[test]
335    fn store_allocation_identity_is_independent_of_schema_order() {
336        let first = Store::new_stable(
337            Def::new("schema_allocation_order", "Users"),
338            "USERS",
339            "users",
340            "schema_allocation_order::Canister",
341            StoreStableMemoryConfig::new(110, 111, 112),
342        );
343        let reordered = Store::new_stable(
344            Def::new("schema_allocation_order", "Users"),
345            "USERS",
346            "users",
347            "schema_allocation_order::Canister",
348            StoreStableMemoryConfig::new(110, 111, 112),
349        );
350
351        assert!(
352            first
353                .stable_data_allocation("test_db")
354                .same_identity_as(&reordered.stable_data_allocation("test_db"))
355        );
356        assert!(
357            first
358                .stable_index_allocation("test_db")
359                .same_identity_as(&reordered.stable_index_allocation("test_db"))
360        );
361        assert!(
362            first
363                .stable_schema_allocation("test_db")
364                .same_identity_as(&reordered.stable_schema_allocation("test_db"))
365        );
366    }
367
368    #[test]
369    fn adding_store_does_not_change_existing_store_allocation() {
370        let existing = Store::new_stable(
371            Def::new("schema_allocation_add", "Users"),
372            "USERS",
373            "users",
374            "schema_allocation_add::Canister",
375            StoreStableMemoryConfig::new(110, 111, 112),
376        );
377        let _new_store = Store::new_stable(
378            Def::new("schema_allocation_add", "AuditEvents"),
379            "AUDIT_EVENTS",
380            "audit_events",
381            "schema_allocation_add::Canister",
382            StoreStableMemoryConfig::new(120, 121, 122),
383        );
384
385        assert_eq!(existing.stable_data_allocation("test_db").memory_id(), 110);
386        assert_eq!(
387            existing.stable_data_allocation("test_db").stable_key(),
388            "icydb.test_db.users.data.v1"
389        );
390    }
391
392    #[test]
393    fn validate_rejects_same_stable_key_with_different_memory_id() {
394        let canister = insert_canister("schema_store_key_collision", "Canister");
395        let canister_path = "schema_store_key_collision::Canister";
396
397        insert_store(
398            "schema_store_key_collision",
399            "StoreA",
400            "users",
401            canister_path,
402            110,
403            111,
404            112,
405        );
406        insert_store(
407            "schema_store_key_collision",
408            "StoreB",
409            "users",
410            canister_path,
411            120,
412            121,
413            122,
414        );
415
416        let err = canister
417            .validate()
418            .expect_err("stable-key collision must fail");
419
420        let rendered = err.to_string();
421        assert!(
422            rendered.contains("duplicate stable_key `icydb.test_db.users.data.v1`"),
423            "expected duplicate stable-key error, got: {rendered}"
424        );
425    }
426
427    #[test]
428    fn stable_memory_identity_ignores_schema_metadata() {
429        let left = StableMemoryAllocation::with_schema_metadata(
430            110,
431            "icydb.test_db.users.data.v1".to_string(),
432            StableMemoryAllocationMetadata::from_accepted_schema_contract(1, "aaa".to_string()),
433        );
434        let right = StableMemoryAllocation::with_schema_metadata(
435            110,
436            "icydb.test_db.users.data.v1".to_string(),
437            StableMemoryAllocationMetadata::from_accepted_schema_contract(2, "bbb".to_string()),
438        );
439
440        assert!(left.same_identity_as(&right));
441    }
442
443    #[test]
444    fn validate_rejects_app_memory_id_below_canic_reserved_range() {
445        let canister = Canister::new(
446            Def::new("schema_reserved_app_range", "Canister"),
447            "test_db",
448            99,
449            110,
450            99,
451        );
452
453        let err = canister
454            .validate()
455            .expect_err("app memory id below 100 must fail");
456
457        let rendered = err.to_string();
458        assert!(
459            rendered.contains("outside of application memory range 100-254"),
460            "expected app memory range error, got: {rendered}"
461        );
462    }
463
464    #[test]
465    fn stable_keys_reject_canic_prefix() {
466        assert!(!stable_key_is_canonical("canic.test.users.data.v1"));
467    }
468}