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#[derive(Clone, Debug, Serialize)]
17pub struct Store {
18 def: Def,
19 ident: &'static str,
20 name: &'static str,
21 canister: &'static str,
22 storage: StoreStorage,
23}
24
25#[derive(Clone, Debug, Serialize)]
30pub enum StoreStorage {
31 Stable(StoreStableMemoryConfig),
34 Heap(StoreHeapConfig),
36}
37
38impl StoreStorage {
39 #[must_use]
44 pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
45 match self {
46 Self::Stable(config) => Some(config),
47 Self::Heap(_) => None,
48 }
49 }
50}
51
52#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
54pub struct StoreHeapConfig;
55
56impl StoreHeapConfig {
57 #[must_use]
59 pub const fn new() -> Self {
60 Self
61 }
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
66pub struct StoreStableMemoryConfig {
67 data: u8,
68 index: u8,
69 schema: u8,
70}
71
72impl StoreStableMemoryConfig {
73 #[must_use]
76 pub const fn new(data_memory_id: u8, index_memory_id: u8, schema_memory_id: u8) -> Self {
77 Self {
78 data: data_memory_id,
79 index: index_memory_id,
80 schema: schema_memory_id,
81 }
82 }
83
84 #[must_use]
86 pub const fn data_memory_id(self) -> u8 {
87 self.data
88 }
89
90 #[must_use]
92 pub const fn index_memory_id(self) -> u8 {
93 self.index
94 }
95
96 #[must_use]
98 pub const fn schema_memory_id(self) -> u8 {
99 self.schema
100 }
101}
102
103impl Store {
104 #[must_use]
108 pub const fn new_stable(
109 def: Def,
110 ident: &'static str,
111 store_name: &'static str,
112 canister: &'static str,
113 stable: StoreStableMemoryConfig,
114 ) -> Self {
115 Self {
116 def,
117 ident,
118 name: store_name,
119 canister,
120 storage: StoreStorage::Stable(stable),
121 }
122 }
123
124 #[must_use]
126 pub const fn new_heap(
127 def: Def,
128 ident: &'static str,
129 store_name: &'static str,
130 canister: &'static str,
131 heap: StoreHeapConfig,
132 ) -> Self {
133 Self {
134 def,
135 ident,
136 name: store_name,
137 canister,
138 storage: StoreStorage::Heap(heap),
139 }
140 }
141
142 #[must_use]
143 pub const fn def(&self) -> &Def {
144 &self.def
145 }
146
147 #[must_use]
148 pub const fn ident(&self) -> &'static str {
149 self.ident
150 }
151
152 #[must_use]
153 pub const fn store_name(&self) -> &'static str {
154 self.name
155 }
156
157 #[must_use]
158 pub const fn canister(&self) -> &'static str {
159 self.canister
160 }
161
162 #[must_use]
164 pub const fn storage(&self) -> &StoreStorage {
165 &self.storage
166 }
167
168 #[must_use]
170 pub const fn is_stable_storage(&self) -> bool {
171 matches!(self.storage, StoreStorage::Stable(_))
172 }
173
174 #[must_use]
176 pub const fn is_heap_storage(&self) -> bool {
177 matches!(self.storage, StoreStorage::Heap(_))
178 }
179
180 #[must_use]
182 pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
183 self.storage.stable_memory_config()
184 }
185
186 #[must_use]
187 pub const fn stable_data_memory_id(&self) -> u8 {
188 match self.storage {
189 StoreStorage::Stable(config) => config.data_memory_id(),
190 StoreStorage::Heap(_) => panic!("heap stores do not have a stable data memory id"),
191 }
192 }
193
194 #[must_use]
195 pub const fn stable_index_memory_id(&self) -> u8 {
196 match self.storage {
197 StoreStorage::Stable(config) => config.index_memory_id(),
198 StoreStorage::Heap(_) => panic!("heap stores do not have a stable index memory id"),
199 }
200 }
201
202 #[must_use]
203 pub const fn stable_schema_memory_id(&self) -> u8 {
204 match self.storage {
205 StoreStorage::Stable(config) => config.schema_memory_id(),
206 StoreStorage::Heap(_) => panic!("heap stores do not have a stable schema memory id"),
207 }
208 }
209
210 #[must_use]
211 pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
212 self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
213 }
214
215 #[must_use]
218 pub fn stable_data_allocation_with_schema_metadata(
219 &self,
220 memory_namespace: &str,
221 schema_metadata: StableMemoryAllocationMetadata,
222 ) -> StableMemoryAllocation {
223 self.stable_allocation_with_schema_metadata(
224 memory_namespace,
225 StoreMemoryRole::Data,
226 schema_metadata,
227 )
228 }
229
230 #[must_use]
231 pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
232 self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
233 }
234
235 #[must_use]
238 pub fn stable_index_allocation_with_schema_metadata(
239 &self,
240 memory_namespace: &str,
241 schema_metadata: StableMemoryAllocationMetadata,
242 ) -> StableMemoryAllocation {
243 self.stable_allocation_with_schema_metadata(
244 memory_namespace,
245 StoreMemoryRole::Index,
246 schema_metadata,
247 )
248 }
249
250 #[must_use]
251 pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
252 self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
253 }
254
255 #[must_use]
258 pub fn stable_schema_allocation_with_schema_metadata(
259 &self,
260 memory_namespace: &str,
261 schema_metadata: StableMemoryAllocationMetadata,
262 ) -> StableMemoryAllocation {
263 self.stable_allocation_with_schema_metadata(
264 memory_namespace,
265 StoreMemoryRole::Schema,
266 schema_metadata,
267 )
268 }
269
270 #[must_use]
271 pub fn stable_allocation(
272 &self,
273 memory_namespace: &str,
274 role: StoreMemoryRole,
275 ) -> StableMemoryAllocation {
276 let memory_id = match role {
277 StoreMemoryRole::Data => self.stable_data_memory_id(),
278 StoreMemoryRole::Index => self.stable_index_memory_id(),
279 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
280 };
281
282 StableMemoryAllocation::without_schema_metadata(
283 memory_id,
284 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
285 )
286 }
287
288 fn stable_allocation_with_schema_metadata(
289 &self,
290 memory_namespace: &str,
291 role: StoreMemoryRole,
292 schema_metadata: StableMemoryAllocationMetadata,
293 ) -> StableMemoryAllocation {
294 let memory_id = match role {
295 StoreMemoryRole::Data => self.stable_data_memory_id(),
296 StoreMemoryRole::Index => self.stable_index_memory_id(),
297 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
298 };
299
300 StableMemoryAllocation::with_schema_metadata(
301 memory_id,
302 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
303 schema_metadata,
304 )
305 }
306}
307
308#[derive(Clone, Copy, Debug, Eq, PartialEq)]
309pub enum StoreMemoryRole {
310 Data,
311 Index,
312 Schema,
313}
314
315impl StoreMemoryRole {
316 #[must_use]
317 pub const fn as_str(self) -> &'static str {
318 match self {
319 Self::Data => "data",
320 Self::Index => "index",
321 Self::Schema => "schema",
322 }
323 }
324}
325
326#[derive(Clone, Debug, Eq, PartialEq)]
331pub struct StableMemoryAllocationMetadata {
332 schema_version: Option<u32>,
333 schema_fingerprint: Option<String>,
334}
335
336impl StableMemoryAllocationMetadata {
337 const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
338 Self {
339 schema_version,
340 schema_fingerprint,
341 }
342 }
343
344 #[must_use]
346 pub const fn from_accepted_schema_contract(
347 schema_version: u32,
348 schema_fingerprint: String,
349 ) -> Self {
350 Self::new(Some(schema_version), Some(schema_fingerprint))
351 }
352
353 #[must_use]
356 pub const fn absent() -> Self {
357 Self::new(None, None)
358 }
359
360 #[must_use]
362 pub const fn schema_version(&self) -> Option<u32> {
363 self.schema_version
364 }
365
366 #[must_use]
368 pub const fn schema_fingerprint(&self) -> Option<&str> {
369 match &self.schema_fingerprint {
370 Some(value) => Some(value.as_str()),
371 None => None,
372 }
373 }
374}
375
376#[derive(Clone, Debug, Eq, PartialEq)]
381pub struct StableMemoryAllocation {
382 memory_id: u8,
383 stable_key: String,
384 schema_metadata: StableMemoryAllocationMetadata,
385}
386
387impl StableMemoryAllocation {
388 #[must_use]
390 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
391 Self::with_schema_metadata(
392 memory_id,
393 stable_key,
394 StableMemoryAllocationMetadata::absent(),
395 )
396 }
397
398 #[must_use]
403 pub const fn with_schema_metadata(
404 memory_id: u8,
405 stable_key: String,
406 schema_metadata: StableMemoryAllocationMetadata,
407 ) -> Self {
408 Self {
409 memory_id,
410 stable_key,
411 schema_metadata,
412 }
413 }
414
415 #[must_use]
417 pub const fn memory_id(&self) -> u8 {
418 self.memory_id
419 }
420
421 #[must_use]
423 pub const fn stable_key(&self) -> &str {
424 self.stable_key.as_str()
425 }
426
427 #[must_use]
429 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
430 &self.schema_metadata
431 }
432
433 #[must_use]
435 pub const fn schema_version(&self) -> Option<u32> {
436 self.schema_metadata.schema_version()
437 }
438
439 #[must_use]
441 pub const fn schema_fingerprint(&self) -> Option<&str> {
442 self.schema_metadata.schema_fingerprint()
443 }
444
445 #[must_use]
450 pub fn same_identity_as(&self, other: &Self) -> bool {
451 self.memory_id == other.memory_id && self.stable_key == other.stable_key
452 }
453}
454
455#[must_use]
456pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
457 format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
458}
459
460impl MacroNode for Store {
461 fn as_any(&self) -> &dyn std::any::Any {
462 self
463 }
464}
465
466impl ValidateNode for Store {
467 fn validate(&self) -> Result<(), ErrorTree> {
468 let mut errs = ErrorTree::new();
469 let schema = schema_read();
470
471 match schema.cast_node::<Canister>(self.canister()) {
472 Ok(canister) => {
473 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
474 match self.storage() {
475 StoreStorage::Stable(config) => {
476 validate_stable_memory_config(&mut errs, self, *config, canister);
477 }
478 StoreStorage::Heap(_) => {}
479 }
480 }
481 Err(e) => errs.add(e),
482 }
483
484 errs.result()
485 }
486}
487
488fn validate_stable_memory_config(
489 errs: &mut ErrorTree,
490 store: &Store,
491 config: StoreStableMemoryConfig,
492 canister: &Canister,
493) {
494 validate_stable_memory_role(
495 errs,
496 "data_memory_id",
497 "data stable key",
498 config.data_memory_id(),
499 store
500 .stable_data_allocation(canister.memory_namespace())
501 .stable_key(),
502 canister,
503 );
504 validate_stable_memory_role(
505 errs,
506 "index_memory_id",
507 "index stable key",
508 config.index_memory_id(),
509 store
510 .stable_index_allocation(canister.memory_namespace())
511 .stable_key(),
512 canister,
513 );
514 validate_stable_memory_role(
515 errs,
516 "schema_memory_id",
517 "schema stable key",
518 config.schema_memory_id(),
519 store
520 .stable_schema_allocation(canister.memory_namespace())
521 .stable_key(),
522 canister,
523 );
524
525 if config.data_memory_id() == config.index_memory_id() {
526 err!(
527 errs,
528 "data_memory_id and index_memory_id must differ (both are {})",
529 config.data_memory_id(),
530 );
531 }
532 if config.data_memory_id() == config.schema_memory_id() {
533 err!(
534 errs,
535 "data_memory_id and schema_memory_id must differ (both are {})",
536 config.data_memory_id(),
537 );
538 }
539 if config.index_memory_id() == config.schema_memory_id() {
540 err!(
541 errs,
542 "index_memory_id and schema_memory_id must differ (both are {})",
543 config.index_memory_id(),
544 );
545 }
546}
547
548fn validate_stable_memory_role(
549 errs: &mut ErrorTree,
550 memory_label: &str,
551 stable_key_label: &str,
552 memory_id: u8,
553 stable_key: &str,
554 canister: &Canister,
555) {
556 validate_memory_id_in_range(
557 errs,
558 memory_label,
559 memory_id,
560 canister.memory_min(),
561 canister.memory_max(),
562 );
563 validate_app_memory_id(errs, memory_label, memory_id);
564 validate_memory_id_not_reserved(errs, memory_label, memory_id);
565 validate_stable_key(errs, stable_key_label, stable_key);
566}
567
568impl VisitableNode for Store {
569 fn route_key(&self) -> String {
570 self.def().path()
571 }
572
573 fn drive<V: Visitor>(&self, v: &mut V) {
574 self.def().accept(v);
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use crate::{
581 build::schema_write,
582 node::{Canister, SchemaNode},
583 };
584
585 use super::*;
586
587 fn insert_canister(path_module: &'static str, ident: &'static str) {
588 schema_write().insert_node(SchemaNode::Canister(Canister::new(
589 Def::new(path_module, ident),
590 "test_db",
591 100,
592 254,
593 254,
594 )));
595 }
596
597 #[test]
598 fn store_stable_keys_use_durable_icydb_shape() {
599 let store = Store::new_stable(
600 Def::new("demo::rpg", "CharacterStore"),
601 "CHARACTER_STORE",
602 "characters",
603 "demo::rpg::Canister",
604 StoreStableMemoryConfig::new(110, 111, 112),
605 );
606
607 assert_eq!(
608 store.stable_data_allocation("demo_rpg").stable_key(),
609 "icydb.demo_rpg.characters.data.v1",
610 );
611 assert_eq!(
612 store.stable_index_allocation("demo_rpg").stable_key(),
613 "icydb.demo_rpg.characters.index.v1",
614 );
615 assert_eq!(
616 store.stable_schema_allocation("demo_rpg").stable_key(),
617 "icydb.demo_rpg.characters.schema.v1",
618 );
619 }
620
621 #[test]
622 fn store_allocations_default_to_absent_schema_metadata() {
623 let store = Store::new_stable(
624 Def::new("demo::rpg", "CharacterStore"),
625 "CHARACTER_STORE",
626 "characters",
627 "demo::rpg::Canister",
628 StoreStableMemoryConfig::new(110, 111, 112),
629 );
630
631 for allocation in [
632 store.stable_data_allocation("demo_rpg"),
633 store.stable_index_allocation("demo_rpg"),
634 store.stable_schema_allocation("demo_rpg"),
635 ] {
636 assert_eq!(allocation.schema_version(), None);
637 assert_eq!(allocation.schema_fingerprint(), None);
638 assert_eq!(
639 allocation.schema_metadata(),
640 &StableMemoryAllocationMetadata::absent()
641 );
642 }
643 }
644
645 #[test]
646 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
647 let store = Store::new_stable(
648 Def::new("demo::rpg", "CharacterStore"),
649 "CHARACTER_STORE",
650 "characters",
651 "demo::rpg::Canister",
652 StoreStableMemoryConfig::new(110, 111, 112),
653 );
654 let data = store.stable_data_allocation_with_schema_metadata(
655 "demo_rpg",
656 StableMemoryAllocationMetadata::from_accepted_schema_contract(
657 7,
658 "data-row-layout".to_string(),
659 ),
660 );
661 let index = store.stable_index_allocation_with_schema_metadata(
662 "demo_rpg",
663 StableMemoryAllocationMetadata::from_accepted_schema_contract(
664 8,
665 "index-catalog".to_string(),
666 ),
667 );
668 let schema = store.stable_schema_allocation_with_schema_metadata(
669 "demo_rpg",
670 StableMemoryAllocationMetadata::from_accepted_schema_contract(
671 10,
672 "schema-catalog".to_string(),
673 ),
674 );
675 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
676 "demo_rpg",
677 StableMemoryAllocationMetadata::from_accepted_schema_contract(
678 9,
679 "data-row-layout-v2".to_string(),
680 ),
681 );
682
683 assert_eq!(data.schema_version(), Some(7));
684 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
685 assert_eq!(index.schema_version(), Some(8));
686 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
687 assert_eq!(schema.schema_version(), Some(10));
688 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
689 assert!(data.same_identity_as(&data_after_reconcile));
690 assert!(!data.same_identity_as(&index));
691 assert!(!data.same_identity_as(&schema));
692 }
693
694 #[test]
695 fn store_owns_explicit_stable_storage_config() {
696 let store = Store::new_stable(
697 Def::new("demo::rpg", "CharacterStore"),
698 "CHARACTER_STORE",
699 "characters",
700 "demo::rpg::Canister",
701 StoreStableMemoryConfig::new(110, 111, 112),
702 );
703
704 assert!(store.is_stable_storage());
705 assert!(store.storage().stable_memory_config().is_some());
706 let stable = store
707 .stable_memory_config()
708 .expect("0.167 model stores stable config explicitly");
709
710 assert_eq!(stable.data_memory_id(), 110);
711 assert_eq!(stable.index_memory_id(), 111);
712 assert_eq!(stable.schema_memory_id(), 112);
713 assert_eq!(store.stable_data_memory_id(), 110);
714 assert_eq!(store.stable_index_memory_id(), 111);
715 assert_eq!(store.stable_schema_memory_id(), 112);
716 }
717
718 #[test]
719 fn store_owns_explicit_heap_storage_config() {
720 insert_canister("store_heap_config", "Canister");
721 let store = Store::new_heap(
722 Def::new("store_heap_config", "Store"),
723 "STORE",
724 "heap_store",
725 "store_heap_config::Canister",
726 StoreHeapConfig::new(),
727 );
728
729 assert!(store.is_heap_storage());
730 assert!(!store.is_stable_storage());
731 assert!(store.stable_memory_config().is_none());
732 assert!(store.validate().is_ok());
733 }
734
735 #[test]
736 fn store_stable_storage_config_rejects_duplicate_role_memory_ids() {
737 insert_canister("store_duplicate_role_memory_ids", "Canister");
738 let store = Store::new_stable(
739 Def::new("store_duplicate_role_memory_ids", "Store"),
740 "STORE",
741 "duplicate_role_memory_ids",
742 "store_duplicate_role_memory_ids::Canister",
743 StoreStableMemoryConfig::new(110, 110, 112),
744 );
745
746 let err = store
747 .validate()
748 .expect_err("duplicate store role memory IDs must fail validation");
749 let rendered = err.to_string();
750
751 assert!(
752 rendered.contains("data_memory_id and index_memory_id must differ"),
753 "expected duplicate role memory-id error, got: {rendered}"
754 );
755 }
756}