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            }
162        }
163
164        errs.result()
165    }
166}
167
168fn assert_unique_memory_allocation(
169    memory_id: u8,
170    stable_key: String,
171    slot: String,
172    canister_path: &str,
173    seen_ids: &mut BTreeMap<u8, (String, String)>,
174    seen_keys: &mut BTreeMap<String, (u8, String)>,
175    errs: &mut ErrorTree,
176) {
177    if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
178        err!(
179            errs,
180            "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
181            memory_id,
182            canister_path,
183            existing_slot,
184            existing_key,
185            slot,
186            stable_key,
187        );
188    } else {
189        seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
190    }
191
192    if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
193        err!(
194            errs,
195            "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
196            stable_key,
197            canister_path,
198            existing_slot,
199            existing_id,
200            slot,
201            memory_id,
202        );
203    } else {
204        seen_keys.insert(stable_key, (memory_id, slot));
205    }
206}
207
208impl VisitableNode for Canister {
209    fn route_key(&self) -> String {
210        self.def().path()
211    }
212}
213
214//
215// TESTS
216//
217
218#[cfg(test)]
219mod tests {
220    use crate::build::schema_write;
221
222    use super::*;
223
224    fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
225        let canister = Canister::new(Def::new(path_module, ident), "test_db", 100, 254, 254);
226        schema_write().insert_node(SchemaNode::Canister(canister.clone()));
227
228        canister
229    }
230
231    fn insert_store(
232        path_module: &'static str,
233        ident: &'static str,
234        store_name: &'static str,
235        canister_path: &'static str,
236        data_memory_id: u8,
237        index_memory_id: u8,
238        schema_memory_id: u8,
239    ) {
240        schema_write().insert_node(SchemaNode::Store(Store::new_stable(
241            Def::new(path_module, ident),
242            ident,
243            store_name,
244            canister_path,
245            StoreStableMemoryConfig::new(data_memory_id, index_memory_id, schema_memory_id),
246        )));
247    }
248
249    #[test]
250    fn validate_rejects_memory_id_collision_between_stores() {
251        let canister = insert_canister("schema_store_collision", "Canister");
252        let canister_path = "schema_store_collision::Canister";
253
254        insert_store(
255            "schema_store_collision",
256            "StoreA",
257            "store_a",
258            canister_path,
259            110,
260            111,
261            112,
262        );
263        insert_store(
264            "schema_store_collision",
265            "StoreB",
266            "store_b",
267            canister_path,
268            113,
269            110,
270            114,
271        ); // collision
272
273        let err = canister
274            .validate()
275            .expect_err("memory-id collision must fail");
276
277        let rendered = err.to_string();
278        assert!(
279            rendered.contains("duplicate memory_id `110`"),
280            "expected duplicate memory-id error, got: {rendered}"
281        );
282    }
283
284    #[test]
285    fn validate_accepts_unique_memory_ids() {
286        let canister = insert_canister("schema_store_unique", "Canister");
287        let canister_path = "schema_store_unique::Canister";
288
289        insert_store(
290            "schema_store_unique",
291            "StoreA",
292            "store_a",
293            canister_path,
294            130,
295            131,
296            132,
297        );
298        insert_store(
299            "schema_store_unique",
300            "StoreB",
301            "store_b",
302            canister_path,
303            133,
304            134,
305            135,
306        );
307
308        canister.validate().expect("unique memory IDs should pass");
309    }
310
311    #[test]
312    fn validate_rejects_reserved_commit_memory_id() {
313        let canister = Canister::new(
314            Def::new("schema_reserved_commit", "Canister"),
315            "test_db",
316            100,
317            254,
318            255,
319        );
320        schema_write().insert_node(SchemaNode::Canister(canister.clone()));
321
322        let err = canister
323            .validate()
324            .expect_err("reserved commit memory id must fail");
325
326        let rendered = err.to_string();
327        assert!(
328            rendered.contains("reserved for stable-structures internals"),
329            "expected reserved-id error, got: {rendered}"
330        );
331    }
332
333    #[test]
334    fn store_allocation_identity_is_independent_of_schema_order() {
335        let first = Store::new_stable(
336            Def::new("schema_allocation_order", "Users"),
337            "USERS",
338            "users",
339            "schema_allocation_order::Canister",
340            StoreStableMemoryConfig::new(110, 111, 112),
341        );
342        let reordered = Store::new_stable(
343            Def::new("schema_allocation_order", "Users"),
344            "USERS",
345            "users",
346            "schema_allocation_order::Canister",
347            StoreStableMemoryConfig::new(110, 111, 112),
348        );
349
350        assert!(
351            first
352                .stable_data_allocation("test_db")
353                .same_identity_as(&reordered.stable_data_allocation("test_db"))
354        );
355        assert!(
356            first
357                .stable_index_allocation("test_db")
358                .same_identity_as(&reordered.stable_index_allocation("test_db"))
359        );
360        assert!(
361            first
362                .stable_schema_allocation("test_db")
363                .same_identity_as(&reordered.stable_schema_allocation("test_db"))
364        );
365    }
366
367    #[test]
368    fn adding_store_does_not_change_existing_store_allocation() {
369        let existing = Store::new_stable(
370            Def::new("schema_allocation_add", "Users"),
371            "USERS",
372            "users",
373            "schema_allocation_add::Canister",
374            StoreStableMemoryConfig::new(110, 111, 112),
375        );
376        let _new_store = Store::new_stable(
377            Def::new("schema_allocation_add", "AuditEvents"),
378            "AUDIT_EVENTS",
379            "audit_events",
380            "schema_allocation_add::Canister",
381            StoreStableMemoryConfig::new(120, 121, 122),
382        );
383
384        assert_eq!(existing.stable_data_allocation("test_db").memory_id(), 110);
385        assert_eq!(
386            existing.stable_data_allocation("test_db").stable_key(),
387            "icydb.test_db.users.data.v1"
388        );
389    }
390
391    #[test]
392    fn validate_rejects_same_stable_key_with_different_memory_id() {
393        let canister = insert_canister("schema_store_key_collision", "Canister");
394        let canister_path = "schema_store_key_collision::Canister";
395
396        insert_store(
397            "schema_store_key_collision",
398            "StoreA",
399            "users",
400            canister_path,
401            110,
402            111,
403            112,
404        );
405        insert_store(
406            "schema_store_key_collision",
407            "StoreB",
408            "users",
409            canister_path,
410            120,
411            121,
412            122,
413        );
414
415        let err = canister
416            .validate()
417            .expect_err("stable-key collision must fail");
418
419        let rendered = err.to_string();
420        assert!(
421            rendered.contains("duplicate stable_key `icydb.test_db.users.data.v1`"),
422            "expected duplicate stable-key error, got: {rendered}"
423        );
424    }
425
426    #[test]
427    fn stable_memory_identity_ignores_schema_metadata() {
428        let left = StableMemoryAllocation::with_schema_metadata(
429            110,
430            "icydb.test_db.users.data.v1".to_string(),
431            StableMemoryAllocationMetadata::from_accepted_schema_contract(1, "aaa".to_string()),
432        );
433        let right = StableMemoryAllocation::with_schema_metadata(
434            110,
435            "icydb.test_db.users.data.v1".to_string(),
436            StableMemoryAllocationMetadata::from_accepted_schema_contract(2, "bbb".to_string()),
437        );
438
439        assert!(left.same_identity_as(&right));
440    }
441
442    #[test]
443    fn validate_rejects_app_memory_id_below_canic_reserved_range() {
444        let canister = Canister::new(
445            Def::new("schema_reserved_app_range", "Canister"),
446            "test_db",
447            99,
448            110,
449            99,
450        );
451
452        let err = canister
453            .validate()
454            .expect_err("app memory id below 100 must fail");
455
456        let rendered = err.to_string();
457        assert!(
458            rendered.contains("outside of application memory range 100-254"),
459            "expected app memory range error, got: {rendered}"
460        );
461    }
462
463    #[test]
464    fn stable_keys_reject_canic_prefix() {
465        assert!(!stable_key_is_canonical("canic.test.users.data.v1"));
466    }
467}