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