1use crate::node::{
2 validate_app_memory_id, validate_memory_id_in_range, validate_memory_id_not_reserved,
3 validate_stable_key,
4};
5use crate::prelude::*;
6
7#[derive(Clone, Debug, Serialize)]
17pub struct Store {
18 def: Def,
19 canister: &'static str,
20 storage: StoreStorage,
21}
22
23#[derive(Clone, Debug, Serialize)]
34pub enum StoreStorage {
35 Heap(StoreHeapConfig),
37 Journaled(StoreJournaledMemoryConfig),
40}
41
42impl StoreStorage {
43 #[must_use]
45 pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
46 match self {
47 Self::Journaled(config) => Some(config),
48 Self::Heap(_) => None,
49 }
50 }
51
52 #[must_use]
54 pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
55 match self {
56 Self::Heap(_) => StoreStorageCapabilities::heap(),
57 Self::Journaled(_) => StoreStorageCapabilities::journaled(),
58 }
59 }
60}
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
66pub enum StoreStorageMode {
67 Heap,
69 Journaled,
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
75pub enum AllocationIdentityCapability {
76 Present,
78 Absent,
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
84pub enum StoreDurability {
85 Durable,
87 Volatile,
89}
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
93pub enum StoreRecoveryCapability {
94 StableBasePlusJournalReplay,
97 None,
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
103pub enum CommitParticipation {
104 Durable,
106 LiveOnly,
108}
109
110#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
112pub enum SchemaMetadataCapability {
113 LiveRebuiltMetadata,
116 CanonicalStableHistoryPlusJournalTail,
118}
119
120#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
122pub enum RelationSourceCapability {
123 DurableSource,
125 LiveSource,
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
131pub enum RelationTargetCapability {
132 DurableTarget,
134 VolatileTarget,
136}
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
140pub enum LiveValidationCapability {
141 Supported,
143}
144
145#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
151pub struct StoreStorageCapabilities {
152 storage_mode: StoreStorageMode,
153 allocation_identity: AllocationIdentityCapability,
154 durability: StoreDurability,
155 recovery: StoreRecoveryCapability,
156 commit_participation: CommitParticipation,
157 schema_metadata: SchemaMetadataCapability,
158 relation_source: RelationSourceCapability,
159 relation_target: RelationTargetCapability,
160 live_validation: LiveValidationCapability,
161}
162
163impl StoreStorageCapabilities {
164 #[must_use]
166 pub const fn heap() -> Self {
167 Self {
168 storage_mode: StoreStorageMode::Heap,
169 allocation_identity: AllocationIdentityCapability::Absent,
170 durability: StoreDurability::Volatile,
171 recovery: StoreRecoveryCapability::None,
172 commit_participation: CommitParticipation::LiveOnly,
173 schema_metadata: SchemaMetadataCapability::LiveRebuiltMetadata,
174 relation_source: RelationSourceCapability::LiveSource,
175 relation_target: RelationTargetCapability::VolatileTarget,
176 live_validation: LiveValidationCapability::Supported,
177 }
178 }
179
180 #[must_use]
182 pub const fn journaled() -> Self {
183 Self {
184 storage_mode: StoreStorageMode::Journaled,
185 allocation_identity: AllocationIdentityCapability::Present,
186 durability: StoreDurability::Durable,
187 recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
188 commit_participation: CommitParticipation::Durable,
189 schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
190 relation_source: RelationSourceCapability::DurableSource,
191 relation_target: RelationTargetCapability::DurableTarget,
192 live_validation: LiveValidationCapability::Supported,
193 }
194 }
195
196 #[must_use]
198 pub const fn storage_mode(self) -> StoreStorageMode {
199 self.storage_mode
200 }
201
202 #[must_use]
204 pub const fn allocation_identity(self) -> AllocationIdentityCapability {
205 self.allocation_identity
206 }
207
208 #[must_use]
210 pub const fn durability(self) -> StoreDurability {
211 self.durability
212 }
213
214 #[must_use]
216 pub const fn recovery(self) -> StoreRecoveryCapability {
217 self.recovery
218 }
219
220 #[must_use]
222 pub const fn commit_participation(self) -> CommitParticipation {
223 self.commit_participation
224 }
225
226 #[must_use]
228 pub const fn schema_metadata(self) -> SchemaMetadataCapability {
229 self.schema_metadata
230 }
231
232 #[must_use]
234 pub const fn relation_source(self) -> RelationSourceCapability {
235 self.relation_source
236 }
237
238 #[must_use]
240 pub const fn relation_target(self) -> RelationTargetCapability {
241 self.relation_target
242 }
243
244 #[must_use]
246 pub const fn live_validation(self) -> LiveValidationCapability {
247 self.live_validation
248 }
249
250 #[must_use]
252 pub const fn has_allocation_identity(self) -> bool {
253 matches!(
254 self.allocation_identity,
255 AllocationIdentityCapability::Present
256 )
257 }
258
259 #[must_use]
261 pub const fn participates_in_durable_commit(self) -> bool {
262 matches!(self.commit_participation, CommitParticipation::Durable)
263 }
264
265 #[must_use]
267 pub const fn is_volatile(self) -> bool {
268 matches!(self.durability, StoreDurability::Volatile)
269 }
270}
271
272#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
274pub struct StoreHeapConfig;
275
276impl StoreHeapConfig {
277 #[must_use]
279 pub const fn new() -> Self {
280 Self
281 }
282}
283
284#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
287pub struct StoreJournaledMemoryConfig {
288 data: u8,
289 index: u8,
290 schema: u8,
291 journal: u8,
292}
293
294impl StoreJournaledMemoryConfig {
295 #[must_use]
298 pub const fn new(
299 data_memory_id: u8,
300 index_memory_id: u8,
301 schema_memory_id: u8,
302 journal_memory_id: u8,
303 ) -> Self {
304 Self {
305 data: data_memory_id,
306 index: index_memory_id,
307 schema: schema_memory_id,
308 journal: journal_memory_id,
309 }
310 }
311
312 #[must_use]
314 pub const fn data_memory_id(self) -> u8 {
315 self.data
316 }
317
318 #[must_use]
320 pub const fn index_memory_id(self) -> u8 {
321 self.index
322 }
323
324 #[must_use]
326 pub const fn schema_memory_id(self) -> u8 {
327 self.schema
328 }
329
330 #[must_use]
332 pub const fn journal_memory_id(self) -> u8 {
333 self.journal
334 }
335}
336
337impl Store {
338 #[must_use]
340 pub const fn new_heap(def: Def, canister: &'static str, heap: StoreHeapConfig) -> Self {
341 Self {
342 def,
343 canister,
344 storage: StoreStorage::Heap(heap),
345 }
346 }
347
348 #[must_use]
350 pub const fn new_journaled(
351 def: Def,
352 canister: &'static str,
353 journaled: StoreJournaledMemoryConfig,
354 ) -> Self {
355 Self {
356 def,
357 canister,
358 storage: StoreStorage::Journaled(journaled),
359 }
360 }
361
362 #[must_use]
363 pub const fn def(&self) -> &Def {
364 &self.def
365 }
366
367 #[must_use]
368 pub const fn canister(&self) -> &'static str {
369 self.canister
370 }
371
372 #[must_use]
374 pub const fn storage(&self) -> &StoreStorage {
375 &self.storage
376 }
377
378 #[must_use]
380 pub const fn is_heap_storage(&self) -> bool {
381 matches!(self.storage, StoreStorage::Heap(_))
382 }
383
384 #[must_use]
386 pub const fn is_journaled_storage(&self) -> bool {
387 matches!(self.storage, StoreStorage::Journaled(_))
388 }
389
390 #[must_use]
393 pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
394 self.storage.journaled_memory_config()
395 }
396
397 #[must_use]
399 pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
400 self.storage.storage_capabilities()
401 }
402
403 #[must_use]
409 pub const fn stable_data_memory_id(&self) -> u8 {
410 match self.storage {
411 StoreStorage::Journaled(config) => config.data_memory_id(),
412 StoreStorage::Heap(_) => panic!("heap stores do not have a stable data memory id"),
413 }
414 }
415
416 #[must_use]
422 pub const fn stable_index_memory_id(&self) -> u8 {
423 match self.storage {
424 StoreStorage::Journaled(config) => config.index_memory_id(),
425 StoreStorage::Heap(_) => panic!("heap stores do not have a stable index memory id"),
426 }
427 }
428
429 #[must_use]
435 pub const fn stable_schema_memory_id(&self) -> u8 {
436 match self.storage {
437 StoreStorage::Journaled(config) => config.schema_memory_id(),
438 StoreStorage::Heap(_) => panic!("heap stores do not have a stable schema memory id"),
439 }
440 }
441
442 #[must_use]
448 pub const fn journal_memory_id(&self) -> u8 {
449 match self.storage {
450 StoreStorage::Journaled(config) => config.journal_memory_id(),
451 StoreStorage::Heap(_) => panic!("heap stores do not have a journal memory id"),
452 }
453 }
454
455 #[must_use]
456 pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
457 self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
458 }
459
460 #[must_use]
463 pub fn stable_data_allocation_with_schema_metadata(
464 &self,
465 memory_namespace: &str,
466 schema_metadata: StableMemoryAllocationMetadata,
467 ) -> StableMemoryAllocation {
468 self.stable_allocation_with_schema_metadata(
469 memory_namespace,
470 StoreMemoryRole::Data,
471 schema_metadata,
472 )
473 }
474
475 #[must_use]
476 pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
477 self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
478 }
479
480 #[must_use]
483 pub fn stable_index_allocation_with_schema_metadata(
484 &self,
485 memory_namespace: &str,
486 schema_metadata: StableMemoryAllocationMetadata,
487 ) -> StableMemoryAllocation {
488 self.stable_allocation_with_schema_metadata(
489 memory_namespace,
490 StoreMemoryRole::Index,
491 schema_metadata,
492 )
493 }
494
495 #[must_use]
496 pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
497 self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
498 }
499
500 #[must_use]
502 pub fn journal_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
503 StableMemoryAllocation::without_schema_metadata(
504 self.journal_memory_id(),
505 stable_store_memory_key(memory_namespace, self.journal_memory_id(), "journal"),
506 )
507 }
508
509 #[must_use]
512 pub fn stable_schema_allocation_with_schema_metadata(
513 &self,
514 memory_namespace: &str,
515 schema_metadata: StableMemoryAllocationMetadata,
516 ) -> StableMemoryAllocation {
517 self.stable_allocation_with_schema_metadata(
518 memory_namespace,
519 StoreMemoryRole::Schema,
520 schema_metadata,
521 )
522 }
523
524 #[must_use]
525 pub fn stable_allocation(
526 &self,
527 memory_namespace: &str,
528 role: StoreMemoryRole,
529 ) -> StableMemoryAllocation {
530 let memory_id = match role {
531 StoreMemoryRole::Data => self.stable_data_memory_id(),
532 StoreMemoryRole::Index => self.stable_index_memory_id(),
533 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
534 };
535
536 StableMemoryAllocation::without_schema_metadata(
537 memory_id,
538 stable_store_memory_key(memory_namespace, memory_id, role.as_str()),
539 )
540 }
541
542 fn stable_allocation_with_schema_metadata(
543 &self,
544 memory_namespace: &str,
545 role: StoreMemoryRole,
546 schema_metadata: StableMemoryAllocationMetadata,
547 ) -> StableMemoryAllocation {
548 let memory_id = match role {
549 StoreMemoryRole::Data => self.stable_data_memory_id(),
550 StoreMemoryRole::Index => self.stable_index_memory_id(),
551 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
552 };
553
554 StableMemoryAllocation::with_schema_metadata(
555 memory_id,
556 stable_store_memory_key(memory_namespace, memory_id, role.as_str()),
557 schema_metadata,
558 )
559 }
560}
561
562#[derive(Clone, Copy, Debug, Eq, PartialEq)]
563pub enum StoreMemoryRole {
564 Data,
565 Index,
566 Schema,
567}
568
569impl StoreMemoryRole {
570 #[must_use]
571 pub const fn as_str(self) -> &'static str {
572 match self {
573 Self::Data => "data",
574 Self::Index => "index",
575 Self::Schema => "schema",
576 }
577 }
578}
579
580#[derive(Clone, Debug, Eq, PartialEq)]
585pub struct StableMemoryAllocationMetadata {
586 version: Option<u32>,
587 fingerprint_method_version: Option<u8>,
588 fingerprint: Option<String>,
589}
590
591impl StableMemoryAllocationMetadata {
592 const fn new(
593 schema_version: Option<u32>,
594 schema_fingerprint_method_version: Option<u8>,
595 schema_fingerprint: Option<String>,
596 ) -> Self {
597 Self {
598 version: schema_version,
599 fingerprint_method_version: schema_fingerprint_method_version,
600 fingerprint: schema_fingerprint,
601 }
602 }
603
604 #[must_use]
606 pub const fn from_accepted_schema_contract(
607 schema_version: u32,
608 schema_fingerprint_method_version: u8,
609 schema_fingerprint: String,
610 ) -> Self {
611 Self::new(
612 Some(schema_version),
613 Some(schema_fingerprint_method_version),
614 Some(schema_fingerprint),
615 )
616 }
617
618 #[must_use]
621 pub const fn absent() -> Self {
622 Self::new(None, None, None)
623 }
624
625 #[must_use]
627 pub const fn schema_version(&self) -> Option<u32> {
628 self.version
629 }
630
631 #[must_use]
633 pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
634 self.fingerprint_method_version
635 }
636
637 #[must_use]
639 pub const fn schema_fingerprint(&self) -> Option<&str> {
640 match &self.fingerprint {
641 Some(value) => Some(value.as_str()),
642 None => None,
643 }
644 }
645}
646
647#[derive(Clone, Debug, Eq, PartialEq)]
653pub struct StableMemoryAllocation {
654 memory_id: u8,
655 stable_key: String,
656 schema_metadata: StableMemoryAllocationMetadata,
657}
658
659impl StableMemoryAllocation {
660 #[must_use]
662 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
663 Self::with_schema_metadata(
664 memory_id,
665 stable_key,
666 StableMemoryAllocationMetadata::absent(),
667 )
668 }
669
670 #[must_use]
675 pub const fn with_schema_metadata(
676 memory_id: u8,
677 stable_key: String,
678 schema_metadata: StableMemoryAllocationMetadata,
679 ) -> Self {
680 Self {
681 memory_id,
682 stable_key,
683 schema_metadata,
684 }
685 }
686
687 #[must_use]
689 pub const fn memory_id(&self) -> u8 {
690 self.memory_id
691 }
692
693 #[must_use]
695 pub const fn stable_key(&self) -> &str {
696 self.stable_key.as_str()
697 }
698
699 #[must_use]
701 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
702 &self.schema_metadata
703 }
704
705 #[must_use]
707 pub const fn schema_version(&self) -> Option<u32> {
708 self.schema_metadata.schema_version()
709 }
710
711 #[must_use]
713 pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
714 self.schema_metadata.schema_fingerprint_method_version()
715 }
716
717 #[must_use]
719 pub const fn schema_fingerprint(&self) -> Option<&str> {
720 self.schema_metadata.schema_fingerprint()
721 }
722
723 #[must_use]
728 pub fn same_identity_as(&self, other: &Self) -> bool {
729 self.memory_id == other.memory_id && self.stable_key == other.stable_key
730 }
731}
732
733#[must_use]
734pub fn stable_memory_key(memory_namespace: &str, allocation: &str, role: &str) -> String {
735 format!("icydb.{memory_namespace}.{allocation}.{role}.v1")
736}
737
738#[must_use]
739fn stable_store_memory_key(memory_namespace: &str, memory_id: u8, role: &str) -> String {
740 format!("icydb.{memory_namespace}.memory_{memory_id}.{role}.v1")
742}
743
744impl MacroNode for Store {
745 fn as_any(&self) -> &dyn std::any::Any {
746 self
747 }
748}
749
750impl ValidateNode for Store {
751 fn validate(&self) -> Result<(), ErrorTree> {
752 let mut errs = ErrorTree::new();
753
754 {
755 let schema = schema_read();
756
757 match schema.cast_node::<Canister>(self.canister()) {
758 Ok(canister) => match self.storage() {
759 StoreStorage::Heap(_) => {}
760 StoreStorage::Journaled(config) => {
761 validate_journaled_memory_config(&mut errs, self, *config, canister);
762 }
763 },
764 Err(e) => errs.add(e),
765 }
766 }
767
768 errs.result()
769 }
770}
771
772fn validate_journaled_memory_config(
773 errs: &mut ErrorTree,
774 store: &Store,
775 config: StoreJournaledMemoryConfig,
776 canister: &Canister,
777) {
778 validate_stable_memory_role(
779 errs,
780 "data_memory_id",
781 "data stable key",
782 config.data_memory_id(),
783 store
784 .stable_data_allocation(canister.memory_namespace())
785 .stable_key(),
786 canister,
787 );
788 validate_stable_memory_role(
789 errs,
790 "index_memory_id",
791 "index stable key",
792 config.index_memory_id(),
793 store
794 .stable_index_allocation(canister.memory_namespace())
795 .stable_key(),
796 canister,
797 );
798 validate_stable_memory_role(
799 errs,
800 "schema_memory_id",
801 "schema stable key",
802 config.schema_memory_id(),
803 store
804 .stable_schema_allocation(canister.memory_namespace())
805 .stable_key(),
806 canister,
807 );
808 validate_stable_memory_role(
809 errs,
810 "journal_memory_id",
811 "journal stable key",
812 config.journal_memory_id(),
813 store
814 .journal_allocation(canister.memory_namespace())
815 .stable_key(),
816 canister,
817 );
818
819 validate_distinct_journaled_memory_ids(errs, config);
820}
821
822fn validate_distinct_journaled_memory_ids(
823 errs: &mut ErrorTree,
824 config: StoreJournaledMemoryConfig,
825) {
826 let roles = [
827 ("data_memory_id", config.data_memory_id()),
828 ("index_memory_id", config.index_memory_id()),
829 ("schema_memory_id", config.schema_memory_id()),
830 ("journal_memory_id", config.journal_memory_id()),
831 ];
832
833 for (idx, (left_label, left_id)) in roles.iter().enumerate() {
834 for (right_label, right_id) in roles.iter().skip(idx + 1) {
835 if left_id == right_id {
836 err!(
837 errs,
838 "{} and {} must differ (both are {})",
839 left_label,
840 right_label,
841 left_id,
842 );
843 }
844 }
845 }
846}
847
848fn validate_stable_memory_role(
849 errs: &mut ErrorTree,
850 memory_label: &str,
851 stable_key_label: &str,
852 memory_id: u8,
853 stable_key: &str,
854 canister: &Canister,
855) {
856 validate_memory_id_in_range(
857 errs,
858 memory_label,
859 memory_id,
860 canister.memory_min(),
861 canister.memory_max(),
862 );
863 validate_app_memory_id(errs, memory_label, memory_id);
864 validate_memory_id_not_reserved(errs, memory_label, memory_id);
865 validate_stable_key(errs, stable_key_label, stable_key);
866}
867
868impl VisitableNode for Store {
869 fn route_key(&self) -> String {
870 self.def().path()
871 }
872
873 fn drive<V: Visitor>(&self, v: &mut V) {
874 self.def().accept(v);
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 use crate::{
881 build::schema_write,
882 node::{Canister, SchemaNode},
883 };
884
885 use super::*;
886
887 fn insert_canister(path_module: &'static str, ident: &'static str) {
888 schema_write().insert_node(SchemaNode::Canister(Canister::new(
889 Def::new(path_module, ident),
890 "test_db",
891 100,
892 254,
893 254,
894 253,
895 )));
896 }
897
898 #[test]
899 fn store_allocations_default_to_absent_schema_metadata() {
900 let store = Store::new_journaled(
901 Def::new("demo::rpg", "CharacterStore"),
902 "demo::rpg::Canister",
903 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
904 );
905
906 for allocation in [
907 store.stable_data_allocation("demo_rpg"),
908 store.stable_index_allocation("demo_rpg"),
909 store.stable_schema_allocation("demo_rpg"),
910 store.journal_allocation("demo_rpg"),
911 ] {
912 assert_eq!(allocation.schema_version(), None);
913 assert_eq!(allocation.schema_fingerprint_method_version(), None);
914 assert_eq!(allocation.schema_fingerprint(), None);
915 assert_eq!(
916 allocation.schema_metadata(),
917 &StableMemoryAllocationMetadata::absent()
918 );
919 }
920 }
921
922 #[test]
923 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
924 let store = Store::new_journaled(
925 Def::new("demo::rpg", "CharacterStore"),
926 "demo::rpg::Canister",
927 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
928 );
929 let data = store.stable_data_allocation_with_schema_metadata(
930 "demo_rpg",
931 StableMemoryAllocationMetadata::from_accepted_schema_contract(
932 7,
933 1,
934 "data-row-layout".to_string(),
935 ),
936 );
937 let index = store.stable_index_allocation_with_schema_metadata(
938 "demo_rpg",
939 StableMemoryAllocationMetadata::from_accepted_schema_contract(
940 8,
941 1,
942 "index-catalog".to_string(),
943 ),
944 );
945 let schema = store.stable_schema_allocation_with_schema_metadata(
946 "demo_rpg",
947 StableMemoryAllocationMetadata::from_accepted_schema_contract(
948 10,
949 1,
950 "schema-catalog".to_string(),
951 ),
952 );
953 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
954 "demo_rpg",
955 StableMemoryAllocationMetadata::from_accepted_schema_contract(
956 9,
957 1,
958 "data-row-layout-changed".to_string(),
959 ),
960 );
961
962 assert_eq!(data.schema_version(), Some(7));
963 assert_eq!(data.schema_fingerprint_method_version(), Some(1));
964 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
965 assert_eq!(index.schema_version(), Some(8));
966 assert_eq!(index.schema_fingerprint_method_version(), Some(1));
967 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
968 assert_eq!(schema.schema_version(), Some(10));
969 assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
970 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
971 assert!(data.same_identity_as(&data_after_reconcile));
972 assert!(!data.same_identity_as(&index));
973 assert!(!data.same_identity_as(&schema));
974 }
975
976 #[test]
977 fn store_owns_explicit_heap_storage_config() {
978 insert_canister("store_heap_config", "Canister");
979 let store = Store::new_heap(
980 Def::new("store_heap_config", "Store"),
981 "store_heap_config::Canister",
982 StoreHeapConfig::new(),
983 );
984
985 assert!(store.is_heap_storage());
986 assert!(store.validate().is_ok());
987 }
988
989 #[test]
990 fn heap_store_storage_capabilities_describe_volatile_contract() {
991 let store = Store::new_heap(
992 Def::new("store_heap_capabilities", "Store"),
993 "store_heap_capabilities::Canister",
994 StoreHeapConfig::new(),
995 );
996 let capabilities = store.storage_capabilities();
997
998 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
999 assert_eq!(
1000 capabilities.allocation_identity(),
1001 AllocationIdentityCapability::Absent,
1002 );
1003 assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1004 assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1005 assert_eq!(
1006 capabilities.commit_participation(),
1007 CommitParticipation::LiveOnly,
1008 );
1009 assert_eq!(
1010 capabilities.schema_metadata(),
1011 SchemaMetadataCapability::LiveRebuiltMetadata,
1012 );
1013 assert_eq!(
1014 capabilities.relation_source(),
1015 RelationSourceCapability::LiveSource,
1016 );
1017 assert_eq!(
1018 capabilities.relation_target(),
1019 RelationTargetCapability::VolatileTarget,
1020 );
1021 assert_eq!(
1022 capabilities.live_validation(),
1023 LiveValidationCapability::Supported,
1024 );
1025 assert!(!capabilities.has_allocation_identity());
1026 assert!(!capabilities.participates_in_durable_commit());
1027 assert!(capabilities.is_volatile());
1028 }
1029
1030 #[test]
1031 fn store_owns_explicit_journaled_storage_config() {
1032 insert_canister("store_journaled_config", "Canister");
1033 let store = Store::new_journaled(
1034 Def::new("store_journaled_config", "Store"),
1035 "store_journaled_config::Canister",
1036 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1037 );
1038
1039 assert!(store.is_journaled_storage());
1040 assert!(!store.is_heap_storage());
1041 let journaled = store
1042 .journaled_memory_config()
1043 .expect("journaled model stores four-role config explicitly");
1044
1045 assert_eq!(journaled.data_memory_id(), 110);
1046 assert_eq!(journaled.index_memory_id(), 111);
1047 assert_eq!(journaled.schema_memory_id(), 112);
1048 assert_eq!(journaled.journal_memory_id(), 113);
1049 assert_eq!(store.stable_data_memory_id(), 110);
1050 assert_eq!(store.stable_index_memory_id(), 111);
1051 assert_eq!(store.stable_schema_memory_id(), 112);
1052 assert_eq!(store.journal_memory_id(), 113);
1053 assert!(store.validate().is_ok());
1054 }
1055
1056 #[test]
1057 fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
1058 let store = Store::new_journaled(
1059 Def::new("store_journaled_capabilities", "Store"),
1060 "store_journaled_capabilities::Canister",
1061 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1062 );
1063 let capabilities = store.storage_capabilities();
1064
1065 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1066 assert_eq!(
1067 capabilities.allocation_identity(),
1068 AllocationIdentityCapability::Present,
1069 );
1070 assert_eq!(capabilities.durability(), StoreDurability::Durable);
1071 assert_eq!(
1072 capabilities.recovery(),
1073 StoreRecoveryCapability::StableBasePlusJournalReplay,
1074 );
1075 assert_eq!(
1076 capabilities.commit_participation(),
1077 CommitParticipation::Durable,
1078 );
1079 assert_eq!(
1080 capabilities.schema_metadata(),
1081 SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1082 );
1083 assert_eq!(
1084 capabilities.relation_source(),
1085 RelationSourceCapability::DurableSource,
1086 );
1087 assert_eq!(
1088 capabilities.relation_target(),
1089 RelationTargetCapability::DurableTarget,
1090 );
1091 assert_eq!(
1092 capabilities.live_validation(),
1093 LiveValidationCapability::Supported,
1094 );
1095 assert!(capabilities.has_allocation_identity());
1096 assert!(capabilities.participates_in_durable_commit());
1097 assert!(!capabilities.is_volatile());
1098 }
1099
1100 #[test]
1101 fn journaled_store_allocations_use_role_named_stable_keys() {
1102 let store = Store::new_journaled(
1103 Def::new("demo::rpg", "CharacterStore"),
1104 "demo::rpg::Canister",
1105 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1106 );
1107
1108 assert_eq!(
1109 store.stable_data_allocation("demo_rpg").stable_key(),
1110 "icydb.demo_rpg.memory_110.data.v1",
1111 );
1112 assert_eq!(
1113 store.stable_index_allocation("demo_rpg").stable_key(),
1114 "icydb.demo_rpg.memory_111.index.v1",
1115 );
1116 assert_eq!(
1117 store.stable_schema_allocation("demo_rpg").stable_key(),
1118 "icydb.demo_rpg.memory_112.schema.v1",
1119 );
1120 assert_eq!(
1121 store.journal_allocation("demo_rpg").stable_key(),
1122 "icydb.demo_rpg.memory_113.journal.v1",
1123 );
1124 }
1125
1126 #[test]
1127 fn storage_capabilities_are_not_allocation_identity() {
1128 let store_a = Store::new_journaled(
1129 Def::new("demo::rpg", "CharacterStore"),
1130 "demo::rpg::Canister",
1131 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1132 );
1133 let store_b = Store::new_journaled(
1134 Def::new("demo::rpg", "InventoryStore"),
1135 "demo::rpg::Canister",
1136 StoreJournaledMemoryConfig::new(120, 121, 122, 123),
1137 );
1138
1139 assert_eq!(
1140 store_a.storage_capabilities(),
1141 store_b.storage_capabilities()
1142 );
1143 assert_ne!(
1144 store_a.stable_data_allocation("demo_rpg"),
1145 store_b.stable_data_allocation("demo_rpg"),
1146 "stable allocation identity must remain separate from capabilities",
1147 );
1148 }
1149
1150 #[test]
1151 fn capability_consumers_use_axes_not_storage_mode() {
1152 const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1153 match capabilities.commit_participation() {
1154 CommitParticipation::Durable => "durable",
1155 CommitParticipation::LiveOnly => "live-only",
1156 }
1157 }
1158
1159 let future_durable_heap_mode = StoreStorageCapabilities {
1160 storage_mode: StoreStorageMode::Heap,
1161 allocation_identity: AllocationIdentityCapability::Present,
1162 durability: StoreDurability::Durable,
1163 recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1164 commit_participation: CommitParticipation::Durable,
1165 schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1166 relation_source: RelationSourceCapability::DurableSource,
1167 relation_target: RelationTargetCapability::DurableTarget,
1168 live_validation: LiveValidationCapability::Supported,
1169 };
1170
1171 assert_eq!(commit_label(future_durable_heap_mode), "durable");
1172 assert!(future_durable_heap_mode.participates_in_durable_commit());
1173 assert_eq!(
1174 future_durable_heap_mode.storage_mode(),
1175 StoreStorageMode::Heap,
1176 "the diagnostic storage mode must not drive commit policy",
1177 );
1178 }
1179
1180 #[test]
1181 fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1182 insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1183 let store = Store::new_journaled(
1184 Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1185 "store_duplicate_journaled_role_memory_ids::Canister",
1186 StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1187 );
1188
1189 let err = store
1190 .validate()
1191 .expect_err("duplicate journaled role memory IDs must fail validation");
1192 let rendered = err.to_string();
1193
1194 assert!(
1195 rendered.contains("schema_memory_id and journal_memory_id must differ"),
1196 "expected duplicate journaled role memory-id error, got: {rendered}"
1197 );
1198 }
1199}