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