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 #[must_use]
53 pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
54 match self {
55 Self::Stable(_) => StoreStorageCapabilities::stable(),
56 Self::Heap(_) => StoreStorageCapabilities::heap(),
57 }
58 }
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
65pub enum StoreStorageMode {
66 Stable,
68 Heap,
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
74pub enum AllocationIdentityCapability {
75 Present,
77 Absent,
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
83pub enum StoreDurability {
84 Durable,
86 Volatile,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
92pub enum StoreRecoveryCapability {
93 StableCommitReplay,
95 None,
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
101pub enum CommitParticipation {
102 Durable,
104 LiveOnly,
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
110pub enum SchemaMetadataCapability {
111 DurableAcceptedHistory,
113 LiveRebuiltMetadata,
115}
116
117#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
119pub enum RelationSourceCapability {
120 DurableSource,
122 LiveSource,
124}
125
126#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
128pub enum RelationTargetCapability {
129 DurableTarget,
131 VolatileTarget,
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
137pub enum LiveValidationCapability {
138 Supported,
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
148pub struct StoreStorageCapabilities {
149 storage_mode: StoreStorageMode,
150 allocation_identity: AllocationIdentityCapability,
151 durability: StoreDurability,
152 recovery: StoreRecoveryCapability,
153 commit_participation: CommitParticipation,
154 schema_metadata: SchemaMetadataCapability,
155 relation_source: RelationSourceCapability,
156 relation_target: RelationTargetCapability,
157 live_validation: LiveValidationCapability,
158}
159
160impl StoreStorageCapabilities {
161 #[must_use]
163 pub const fn stable() -> Self {
164 Self {
165 storage_mode: StoreStorageMode::Stable,
166 allocation_identity: AllocationIdentityCapability::Present,
167 durability: StoreDurability::Durable,
168 recovery: StoreRecoveryCapability::StableCommitReplay,
169 commit_participation: CommitParticipation::Durable,
170 schema_metadata: SchemaMetadataCapability::DurableAcceptedHistory,
171 relation_source: RelationSourceCapability::DurableSource,
172 relation_target: RelationTargetCapability::DurableTarget,
173 live_validation: LiveValidationCapability::Supported,
174 }
175 }
176
177 #[must_use]
179 pub const fn heap() -> Self {
180 Self {
181 storage_mode: StoreStorageMode::Heap,
182 allocation_identity: AllocationIdentityCapability::Absent,
183 durability: StoreDurability::Volatile,
184 recovery: StoreRecoveryCapability::None,
185 commit_participation: CommitParticipation::LiveOnly,
186 schema_metadata: SchemaMetadataCapability::LiveRebuiltMetadata,
187 relation_source: RelationSourceCapability::LiveSource,
188 relation_target: RelationTargetCapability::VolatileTarget,
189 live_validation: LiveValidationCapability::Supported,
190 }
191 }
192
193 #[must_use]
195 pub const fn storage_mode(self) -> StoreStorageMode {
196 self.storage_mode
197 }
198
199 #[must_use]
201 pub const fn allocation_identity(self) -> AllocationIdentityCapability {
202 self.allocation_identity
203 }
204
205 #[must_use]
207 pub const fn durability(self) -> StoreDurability {
208 self.durability
209 }
210
211 #[must_use]
213 pub const fn recovery(self) -> StoreRecoveryCapability {
214 self.recovery
215 }
216
217 #[must_use]
219 pub const fn commit_participation(self) -> CommitParticipation {
220 self.commit_participation
221 }
222
223 #[must_use]
225 pub const fn schema_metadata(self) -> SchemaMetadataCapability {
226 self.schema_metadata
227 }
228
229 #[must_use]
231 pub const fn relation_source(self) -> RelationSourceCapability {
232 self.relation_source
233 }
234
235 #[must_use]
237 pub const fn relation_target(self) -> RelationTargetCapability {
238 self.relation_target
239 }
240
241 #[must_use]
243 pub const fn live_validation(self) -> LiveValidationCapability {
244 self.live_validation
245 }
246
247 #[must_use]
249 pub const fn has_allocation_identity(self) -> bool {
250 matches!(
251 self.allocation_identity,
252 AllocationIdentityCapability::Present
253 )
254 }
255
256 #[must_use]
258 pub const fn participates_in_durable_commit(self) -> bool {
259 matches!(self.commit_participation, CommitParticipation::Durable)
260 }
261
262 #[must_use]
264 pub const fn is_volatile(self) -> bool {
265 matches!(self.durability, StoreDurability::Volatile)
266 }
267}
268
269#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
271pub struct StoreHeapConfig;
272
273impl StoreHeapConfig {
274 #[must_use]
276 pub const fn new() -> Self {
277 Self
278 }
279}
280
281#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
283pub struct StoreStableMemoryConfig {
284 data: u8,
285 index: u8,
286 schema: u8,
287}
288
289impl StoreStableMemoryConfig {
290 #[must_use]
293 pub const fn new(data_memory_id: u8, index_memory_id: u8, schema_memory_id: u8) -> Self {
294 Self {
295 data: data_memory_id,
296 index: index_memory_id,
297 schema: schema_memory_id,
298 }
299 }
300
301 #[must_use]
303 pub const fn data_memory_id(self) -> u8 {
304 self.data
305 }
306
307 #[must_use]
309 pub const fn index_memory_id(self) -> u8 {
310 self.index
311 }
312
313 #[must_use]
315 pub const fn schema_memory_id(self) -> u8 {
316 self.schema
317 }
318}
319
320impl Store {
321 #[must_use]
325 pub const fn new_stable(
326 def: Def,
327 ident: &'static str,
328 store_name: &'static str,
329 canister: &'static str,
330 stable: StoreStableMemoryConfig,
331 ) -> Self {
332 Self {
333 def,
334 ident,
335 name: store_name,
336 canister,
337 storage: StoreStorage::Stable(stable),
338 }
339 }
340
341 #[must_use]
343 pub const fn new_heap(
344 def: Def,
345 ident: &'static str,
346 store_name: &'static str,
347 canister: &'static str,
348 heap: StoreHeapConfig,
349 ) -> Self {
350 Self {
351 def,
352 ident,
353 name: store_name,
354 canister,
355 storage: StoreStorage::Heap(heap),
356 }
357 }
358
359 #[must_use]
360 pub const fn def(&self) -> &Def {
361 &self.def
362 }
363
364 #[must_use]
365 pub const fn ident(&self) -> &'static str {
366 self.ident
367 }
368
369 #[must_use]
370 pub const fn store_name(&self) -> &'static str {
371 self.name
372 }
373
374 #[must_use]
375 pub const fn canister(&self) -> &'static str {
376 self.canister
377 }
378
379 #[must_use]
381 pub const fn storage(&self) -> &StoreStorage {
382 &self.storage
383 }
384
385 #[must_use]
387 pub const fn is_stable_storage(&self) -> bool {
388 matches!(self.storage, StoreStorage::Stable(_))
389 }
390
391 #[must_use]
393 pub const fn is_heap_storage(&self) -> bool {
394 matches!(self.storage, StoreStorage::Heap(_))
395 }
396
397 #[must_use]
399 pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
400 self.storage.stable_memory_config()
401 }
402
403 #[must_use]
405 pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
406 self.storage.storage_capabilities()
407 }
408
409 #[must_use]
410 pub const fn stable_data_memory_id(&self) -> u8 {
411 match self.storage {
412 StoreStorage::Stable(config) => config.data_memory_id(),
413 StoreStorage::Heap(_) => panic!("heap stores do not have a stable data memory id"),
414 }
415 }
416
417 #[must_use]
418 pub const fn stable_index_memory_id(&self) -> u8 {
419 match self.storage {
420 StoreStorage::Stable(config) => config.index_memory_id(),
421 StoreStorage::Heap(_) => panic!("heap stores do not have a stable index memory id"),
422 }
423 }
424
425 #[must_use]
426 pub const fn stable_schema_memory_id(&self) -> u8 {
427 match self.storage {
428 StoreStorage::Stable(config) => config.schema_memory_id(),
429 StoreStorage::Heap(_) => panic!("heap stores do not have a stable schema memory id"),
430 }
431 }
432
433 #[must_use]
434 pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
435 self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
436 }
437
438 #[must_use]
441 pub fn stable_data_allocation_with_schema_metadata(
442 &self,
443 memory_namespace: &str,
444 schema_metadata: StableMemoryAllocationMetadata,
445 ) -> StableMemoryAllocation {
446 self.stable_allocation_with_schema_metadata(
447 memory_namespace,
448 StoreMemoryRole::Data,
449 schema_metadata,
450 )
451 }
452
453 #[must_use]
454 pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
455 self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
456 }
457
458 #[must_use]
461 pub fn stable_index_allocation_with_schema_metadata(
462 &self,
463 memory_namespace: &str,
464 schema_metadata: StableMemoryAllocationMetadata,
465 ) -> StableMemoryAllocation {
466 self.stable_allocation_with_schema_metadata(
467 memory_namespace,
468 StoreMemoryRole::Index,
469 schema_metadata,
470 )
471 }
472
473 #[must_use]
474 pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
475 self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
476 }
477
478 #[must_use]
481 pub fn stable_schema_allocation_with_schema_metadata(
482 &self,
483 memory_namespace: &str,
484 schema_metadata: StableMemoryAllocationMetadata,
485 ) -> StableMemoryAllocation {
486 self.stable_allocation_with_schema_metadata(
487 memory_namespace,
488 StoreMemoryRole::Schema,
489 schema_metadata,
490 )
491 }
492
493 #[must_use]
494 pub fn stable_allocation(
495 &self,
496 memory_namespace: &str,
497 role: StoreMemoryRole,
498 ) -> StableMemoryAllocation {
499 let memory_id = match role {
500 StoreMemoryRole::Data => self.stable_data_memory_id(),
501 StoreMemoryRole::Index => self.stable_index_memory_id(),
502 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
503 };
504
505 StableMemoryAllocation::without_schema_metadata(
506 memory_id,
507 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
508 )
509 }
510
511 fn stable_allocation_with_schema_metadata(
512 &self,
513 memory_namespace: &str,
514 role: StoreMemoryRole,
515 schema_metadata: StableMemoryAllocationMetadata,
516 ) -> StableMemoryAllocation {
517 let memory_id = match role {
518 StoreMemoryRole::Data => self.stable_data_memory_id(),
519 StoreMemoryRole::Index => self.stable_index_memory_id(),
520 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
521 };
522
523 StableMemoryAllocation::with_schema_metadata(
524 memory_id,
525 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
526 schema_metadata,
527 )
528 }
529}
530
531#[derive(Clone, Copy, Debug, Eq, PartialEq)]
532pub enum StoreMemoryRole {
533 Data,
534 Index,
535 Schema,
536}
537
538impl StoreMemoryRole {
539 #[must_use]
540 pub const fn as_str(self) -> &'static str {
541 match self {
542 Self::Data => "data",
543 Self::Index => "index",
544 Self::Schema => "schema",
545 }
546 }
547}
548
549#[derive(Clone, Debug, Eq, PartialEq)]
554pub struct StableMemoryAllocationMetadata {
555 schema_version: Option<u32>,
556 schema_fingerprint: Option<String>,
557}
558
559impl StableMemoryAllocationMetadata {
560 const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
561 Self {
562 schema_version,
563 schema_fingerprint,
564 }
565 }
566
567 #[must_use]
569 pub const fn from_accepted_schema_contract(
570 schema_version: u32,
571 schema_fingerprint: String,
572 ) -> Self {
573 Self::new(Some(schema_version), Some(schema_fingerprint))
574 }
575
576 #[must_use]
579 pub const fn absent() -> Self {
580 Self::new(None, None)
581 }
582
583 #[must_use]
585 pub const fn schema_version(&self) -> Option<u32> {
586 self.schema_version
587 }
588
589 #[must_use]
591 pub const fn schema_fingerprint(&self) -> Option<&str> {
592 match &self.schema_fingerprint {
593 Some(value) => Some(value.as_str()),
594 None => None,
595 }
596 }
597}
598
599#[derive(Clone, Debug, Eq, PartialEq)]
604pub struct StableMemoryAllocation {
605 memory_id: u8,
606 stable_key: String,
607 schema_metadata: StableMemoryAllocationMetadata,
608}
609
610impl StableMemoryAllocation {
611 #[must_use]
613 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
614 Self::with_schema_metadata(
615 memory_id,
616 stable_key,
617 StableMemoryAllocationMetadata::absent(),
618 )
619 }
620
621 #[must_use]
626 pub const fn with_schema_metadata(
627 memory_id: u8,
628 stable_key: String,
629 schema_metadata: StableMemoryAllocationMetadata,
630 ) -> Self {
631 Self {
632 memory_id,
633 stable_key,
634 schema_metadata,
635 }
636 }
637
638 #[must_use]
640 pub const fn memory_id(&self) -> u8 {
641 self.memory_id
642 }
643
644 #[must_use]
646 pub const fn stable_key(&self) -> &str {
647 self.stable_key.as_str()
648 }
649
650 #[must_use]
652 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
653 &self.schema_metadata
654 }
655
656 #[must_use]
658 pub const fn schema_version(&self) -> Option<u32> {
659 self.schema_metadata.schema_version()
660 }
661
662 #[must_use]
664 pub const fn schema_fingerprint(&self) -> Option<&str> {
665 self.schema_metadata.schema_fingerprint()
666 }
667
668 #[must_use]
673 pub fn same_identity_as(&self, other: &Self) -> bool {
674 self.memory_id == other.memory_id && self.stable_key == other.stable_key
675 }
676}
677
678#[must_use]
679pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
680 format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
681}
682
683impl MacroNode for Store {
684 fn as_any(&self) -> &dyn std::any::Any {
685 self
686 }
687}
688
689impl ValidateNode for Store {
690 fn validate(&self) -> Result<(), ErrorTree> {
691 let mut errs = ErrorTree::new();
692 let schema = schema_read();
693
694 match schema.cast_node::<Canister>(self.canister()) {
695 Ok(canister) => {
696 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
697 match self.storage() {
698 StoreStorage::Stable(config) => {
699 validate_stable_memory_config(&mut errs, self, *config, canister);
700 }
701 StoreStorage::Heap(_) => {}
702 }
703 }
704 Err(e) => errs.add(e),
705 }
706
707 errs.result()
708 }
709}
710
711fn validate_stable_memory_config(
712 errs: &mut ErrorTree,
713 store: &Store,
714 config: StoreStableMemoryConfig,
715 canister: &Canister,
716) {
717 validate_stable_memory_role(
718 errs,
719 "data_memory_id",
720 "data stable key",
721 config.data_memory_id(),
722 store
723 .stable_data_allocation(canister.memory_namespace())
724 .stable_key(),
725 canister,
726 );
727 validate_stable_memory_role(
728 errs,
729 "index_memory_id",
730 "index stable key",
731 config.index_memory_id(),
732 store
733 .stable_index_allocation(canister.memory_namespace())
734 .stable_key(),
735 canister,
736 );
737 validate_stable_memory_role(
738 errs,
739 "schema_memory_id",
740 "schema stable key",
741 config.schema_memory_id(),
742 store
743 .stable_schema_allocation(canister.memory_namespace())
744 .stable_key(),
745 canister,
746 );
747
748 if config.data_memory_id() == config.index_memory_id() {
749 err!(
750 errs,
751 "data_memory_id and index_memory_id must differ (both are {})",
752 config.data_memory_id(),
753 );
754 }
755 if config.data_memory_id() == config.schema_memory_id() {
756 err!(
757 errs,
758 "data_memory_id and schema_memory_id must differ (both are {})",
759 config.data_memory_id(),
760 );
761 }
762 if config.index_memory_id() == config.schema_memory_id() {
763 err!(
764 errs,
765 "index_memory_id and schema_memory_id must differ (both are {})",
766 config.index_memory_id(),
767 );
768 }
769}
770
771fn validate_stable_memory_role(
772 errs: &mut ErrorTree,
773 memory_label: &str,
774 stable_key_label: &str,
775 memory_id: u8,
776 stable_key: &str,
777 canister: &Canister,
778) {
779 validate_memory_id_in_range(
780 errs,
781 memory_label,
782 memory_id,
783 canister.memory_min(),
784 canister.memory_max(),
785 );
786 validate_app_memory_id(errs, memory_label, memory_id);
787 validate_memory_id_not_reserved(errs, memory_label, memory_id);
788 validate_stable_key(errs, stable_key_label, stable_key);
789}
790
791impl VisitableNode for Store {
792 fn route_key(&self) -> String {
793 self.def().path()
794 }
795
796 fn drive<V: Visitor>(&self, v: &mut V) {
797 self.def().accept(v);
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use crate::{
804 build::schema_write,
805 node::{Canister, SchemaNode},
806 };
807
808 use super::*;
809
810 fn insert_canister(path_module: &'static str, ident: &'static str) {
811 schema_write().insert_node(SchemaNode::Canister(Canister::new(
812 Def::new(path_module, ident),
813 "test_db",
814 100,
815 254,
816 254,
817 )));
818 }
819
820 #[test]
821 fn store_stable_keys_use_durable_icydb_shape() {
822 let store = Store::new_stable(
823 Def::new("demo::rpg", "CharacterStore"),
824 "CHARACTER_STORE",
825 "characters",
826 "demo::rpg::Canister",
827 StoreStableMemoryConfig::new(110, 111, 112),
828 );
829
830 assert_eq!(
831 store.stable_data_allocation("demo_rpg").stable_key(),
832 "icydb.demo_rpg.characters.data.v1",
833 );
834 assert_eq!(
835 store.stable_index_allocation("demo_rpg").stable_key(),
836 "icydb.demo_rpg.characters.index.v1",
837 );
838 assert_eq!(
839 store.stable_schema_allocation("demo_rpg").stable_key(),
840 "icydb.demo_rpg.characters.schema.v1",
841 );
842 }
843
844 #[test]
845 fn store_allocations_default_to_absent_schema_metadata() {
846 let store = Store::new_stable(
847 Def::new("demo::rpg", "CharacterStore"),
848 "CHARACTER_STORE",
849 "characters",
850 "demo::rpg::Canister",
851 StoreStableMemoryConfig::new(110, 111, 112),
852 );
853
854 for allocation in [
855 store.stable_data_allocation("demo_rpg"),
856 store.stable_index_allocation("demo_rpg"),
857 store.stable_schema_allocation("demo_rpg"),
858 ] {
859 assert_eq!(allocation.schema_version(), None);
860 assert_eq!(allocation.schema_fingerprint(), None);
861 assert_eq!(
862 allocation.schema_metadata(),
863 &StableMemoryAllocationMetadata::absent()
864 );
865 }
866 }
867
868 #[test]
869 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
870 let store = Store::new_stable(
871 Def::new("demo::rpg", "CharacterStore"),
872 "CHARACTER_STORE",
873 "characters",
874 "demo::rpg::Canister",
875 StoreStableMemoryConfig::new(110, 111, 112),
876 );
877 let data = store.stable_data_allocation_with_schema_metadata(
878 "demo_rpg",
879 StableMemoryAllocationMetadata::from_accepted_schema_contract(
880 7,
881 "data-row-layout".to_string(),
882 ),
883 );
884 let index = store.stable_index_allocation_with_schema_metadata(
885 "demo_rpg",
886 StableMemoryAllocationMetadata::from_accepted_schema_contract(
887 8,
888 "index-catalog".to_string(),
889 ),
890 );
891 let schema = store.stable_schema_allocation_with_schema_metadata(
892 "demo_rpg",
893 StableMemoryAllocationMetadata::from_accepted_schema_contract(
894 10,
895 "schema-catalog".to_string(),
896 ),
897 );
898 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
899 "demo_rpg",
900 StableMemoryAllocationMetadata::from_accepted_schema_contract(
901 9,
902 "data-row-layout-v2".to_string(),
903 ),
904 );
905
906 assert_eq!(data.schema_version(), Some(7));
907 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
908 assert_eq!(index.schema_version(), Some(8));
909 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
910 assert_eq!(schema.schema_version(), Some(10));
911 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
912 assert!(data.same_identity_as(&data_after_reconcile));
913 assert!(!data.same_identity_as(&index));
914 assert!(!data.same_identity_as(&schema));
915 }
916
917 #[test]
918 fn store_owns_explicit_stable_storage_config() {
919 let store = Store::new_stable(
920 Def::new("demo::rpg", "CharacterStore"),
921 "CHARACTER_STORE",
922 "characters",
923 "demo::rpg::Canister",
924 StoreStableMemoryConfig::new(110, 111, 112),
925 );
926
927 assert!(store.is_stable_storage());
928 assert!(store.storage().stable_memory_config().is_some());
929 let stable = store
930 .stable_memory_config()
931 .expect("0.167 model stores stable config explicitly");
932
933 assert_eq!(stable.data_memory_id(), 110);
934 assert_eq!(stable.index_memory_id(), 111);
935 assert_eq!(stable.schema_memory_id(), 112);
936 assert_eq!(store.stable_data_memory_id(), 110);
937 assert_eq!(store.stable_index_memory_id(), 111);
938 assert_eq!(store.stable_schema_memory_id(), 112);
939 }
940
941 #[test]
942 fn stable_store_storage_capabilities_describe_durable_contract() {
943 let store = Store::new_stable(
944 Def::new("demo::rpg", "CharacterStore"),
945 "CHARACTER_STORE",
946 "characters",
947 "demo::rpg::Canister",
948 StoreStableMemoryConfig::new(110, 111, 112),
949 );
950 let capabilities = store.storage_capabilities();
951
952 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Stable);
953 assert_eq!(
954 capabilities.allocation_identity(),
955 AllocationIdentityCapability::Present,
956 );
957 assert_eq!(capabilities.durability(), StoreDurability::Durable);
958 assert_eq!(
959 capabilities.recovery(),
960 StoreRecoveryCapability::StableCommitReplay,
961 );
962 assert_eq!(
963 capabilities.commit_participation(),
964 CommitParticipation::Durable,
965 );
966 assert_eq!(
967 capabilities.schema_metadata(),
968 SchemaMetadataCapability::DurableAcceptedHistory,
969 );
970 assert_eq!(
971 capabilities.relation_source(),
972 RelationSourceCapability::DurableSource,
973 );
974 assert_eq!(
975 capabilities.relation_target(),
976 RelationTargetCapability::DurableTarget,
977 );
978 assert_eq!(
979 capabilities.live_validation(),
980 LiveValidationCapability::Supported,
981 );
982 assert!(capabilities.has_allocation_identity());
983 assert!(capabilities.participates_in_durable_commit());
984 assert!(!capabilities.is_volatile());
985 }
986
987 #[test]
988 fn store_owns_explicit_heap_storage_config() {
989 insert_canister("store_heap_config", "Canister");
990 let store = Store::new_heap(
991 Def::new("store_heap_config", "Store"),
992 "STORE",
993 "heap_store",
994 "store_heap_config::Canister",
995 StoreHeapConfig::new(),
996 );
997
998 assert!(store.is_heap_storage());
999 assert!(!store.is_stable_storage());
1000 assert!(store.stable_memory_config().is_none());
1001 assert!(store.validate().is_ok());
1002 }
1003
1004 #[test]
1005 fn heap_store_storage_capabilities_describe_volatile_contract() {
1006 let store = Store::new_heap(
1007 Def::new("store_heap_capabilities", "Store"),
1008 "STORE",
1009 "heap_store",
1010 "store_heap_capabilities::Canister",
1011 StoreHeapConfig::new(),
1012 );
1013 let capabilities = store.storage_capabilities();
1014
1015 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
1016 assert_eq!(
1017 capabilities.allocation_identity(),
1018 AllocationIdentityCapability::Absent,
1019 );
1020 assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1021 assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1022 assert_eq!(
1023 capabilities.commit_participation(),
1024 CommitParticipation::LiveOnly,
1025 );
1026 assert_eq!(
1027 capabilities.schema_metadata(),
1028 SchemaMetadataCapability::LiveRebuiltMetadata,
1029 );
1030 assert_eq!(
1031 capabilities.relation_source(),
1032 RelationSourceCapability::LiveSource,
1033 );
1034 assert_eq!(
1035 capabilities.relation_target(),
1036 RelationTargetCapability::VolatileTarget,
1037 );
1038 assert_eq!(
1039 capabilities.live_validation(),
1040 LiveValidationCapability::Supported,
1041 );
1042 assert!(!capabilities.has_allocation_identity());
1043 assert!(!capabilities.participates_in_durable_commit());
1044 assert!(capabilities.is_volatile());
1045 }
1046
1047 #[test]
1048 fn storage_capabilities_are_not_allocation_identity() {
1049 let store_a = Store::new_stable(
1050 Def::new("demo::rpg", "CharacterStore"),
1051 "CHARACTER_STORE",
1052 "characters",
1053 "demo::rpg::Canister",
1054 StoreStableMemoryConfig::new(110, 111, 112),
1055 );
1056 let store_b = Store::new_stable(
1057 Def::new("demo::rpg", "InventoryStore"),
1058 "INVENTORY_STORE",
1059 "inventory",
1060 "demo::rpg::Canister",
1061 StoreStableMemoryConfig::new(120, 121, 122),
1062 );
1063
1064 assert_eq!(
1065 store_a.storage_capabilities(),
1066 store_b.storage_capabilities()
1067 );
1068 assert_ne!(
1069 store_a.stable_data_allocation("demo_rpg"),
1070 store_b.stable_data_allocation("demo_rpg"),
1071 "stable allocation identity must remain separate from capabilities",
1072 );
1073 }
1074
1075 #[test]
1076 fn capability_consumers_use_axes_not_storage_mode() {
1077 const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1078 match capabilities.commit_participation() {
1079 CommitParticipation::Durable => "durable",
1080 CommitParticipation::LiveOnly => "live-only",
1081 }
1082 }
1083
1084 let future_durable_heap_mode = StoreStorageCapabilities {
1085 storage_mode: StoreStorageMode::Heap,
1086 allocation_identity: AllocationIdentityCapability::Present,
1087 durability: StoreDurability::Durable,
1088 recovery: StoreRecoveryCapability::StableCommitReplay,
1089 commit_participation: CommitParticipation::Durable,
1090 schema_metadata: SchemaMetadataCapability::DurableAcceptedHistory,
1091 relation_source: RelationSourceCapability::DurableSource,
1092 relation_target: RelationTargetCapability::DurableTarget,
1093 live_validation: LiveValidationCapability::Supported,
1094 };
1095
1096 assert_eq!(commit_label(future_durable_heap_mode), "durable");
1097 assert!(future_durable_heap_mode.participates_in_durable_commit());
1098 assert_eq!(
1099 future_durable_heap_mode.storage_mode(),
1100 StoreStorageMode::Heap,
1101 "the diagnostic storage mode must not drive commit policy",
1102 );
1103 }
1104
1105 #[test]
1106 fn store_stable_storage_config_rejects_duplicate_role_memory_ids() {
1107 insert_canister("store_duplicate_role_memory_ids", "Canister");
1108 let store = Store::new_stable(
1109 Def::new("store_duplicate_role_memory_ids", "Store"),
1110 "STORE",
1111 "duplicate_role_memory_ids",
1112 "store_duplicate_role_memory_ids::Canister",
1113 StoreStableMemoryConfig::new(110, 110, 112),
1114 );
1115
1116 let err = store
1117 .validate()
1118 .expect_err("duplicate store role memory IDs must fail validation");
1119 let rendered = err.to_string();
1120
1121 assert!(
1122 rendered.contains("data_memory_id and index_memory_id must differ"),
1123 "expected duplicate role memory-id error, got: {rendered}"
1124 );
1125 }
1126}