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 None,
896 )));
897 }
898
899 #[test]
900 fn store_allocations_default_to_absent_schema_metadata() {
901 let store = Store::new_journaled(
902 Def::new("demo::rpg", "CharacterStore"),
903 "demo::rpg::Canister",
904 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
905 );
906
907 for allocation in [
908 store.stable_data_allocation("demo_rpg"),
909 store.stable_index_allocation("demo_rpg"),
910 store.stable_schema_allocation("demo_rpg"),
911 store.journal_allocation("demo_rpg"),
912 ] {
913 assert_eq!(allocation.schema_version(), None);
914 assert_eq!(allocation.schema_fingerprint_method_version(), None);
915 assert_eq!(allocation.schema_fingerprint(), None);
916 assert_eq!(
917 allocation.schema_metadata(),
918 &StableMemoryAllocationMetadata::absent()
919 );
920 }
921 }
922
923 #[test]
924 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
925 let store = Store::new_journaled(
926 Def::new("demo::rpg", "CharacterStore"),
927 "demo::rpg::Canister",
928 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
929 );
930 let data = store.stable_data_allocation_with_schema_metadata(
931 "demo_rpg",
932 StableMemoryAllocationMetadata::from_accepted_schema_contract(
933 7,
934 1,
935 "data-row-layout".to_string(),
936 ),
937 );
938 let index = store.stable_index_allocation_with_schema_metadata(
939 "demo_rpg",
940 StableMemoryAllocationMetadata::from_accepted_schema_contract(
941 8,
942 1,
943 "index-catalog".to_string(),
944 ),
945 );
946 let schema = store.stable_schema_allocation_with_schema_metadata(
947 "demo_rpg",
948 StableMemoryAllocationMetadata::from_accepted_schema_contract(
949 10,
950 1,
951 "schema-catalog".to_string(),
952 ),
953 );
954 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
955 "demo_rpg",
956 StableMemoryAllocationMetadata::from_accepted_schema_contract(
957 9,
958 1,
959 "data-row-layout-changed".to_string(),
960 ),
961 );
962
963 assert_eq!(data.schema_version(), Some(7));
964 assert_eq!(data.schema_fingerprint_method_version(), Some(1));
965 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
966 assert_eq!(index.schema_version(), Some(8));
967 assert_eq!(index.schema_fingerprint_method_version(), Some(1));
968 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
969 assert_eq!(schema.schema_version(), Some(10));
970 assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
971 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
972 assert!(data.same_identity_as(&data_after_reconcile));
973 assert!(!data.same_identity_as(&index));
974 assert!(!data.same_identity_as(&schema));
975 }
976
977 #[test]
978 fn store_owns_explicit_heap_storage_config() {
979 insert_canister("store_heap_config", "Canister");
980 let store = Store::new_heap(
981 Def::new("store_heap_config", "Store"),
982 "store_heap_config::Canister",
983 StoreHeapConfig::new(),
984 );
985
986 assert!(store.is_heap_storage());
987 assert!(store.validate().is_ok());
988 }
989
990 #[test]
991 fn heap_store_storage_capabilities_describe_volatile_contract() {
992 let store = Store::new_heap(
993 Def::new("store_heap_capabilities", "Store"),
994 "store_heap_capabilities::Canister",
995 StoreHeapConfig::new(),
996 );
997 let capabilities = store.storage_capabilities();
998
999 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
1000 assert_eq!(
1001 capabilities.allocation_identity(),
1002 AllocationIdentityCapability::Absent,
1003 );
1004 assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1005 assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1006 assert_eq!(
1007 capabilities.commit_participation(),
1008 CommitParticipation::LiveOnly,
1009 );
1010 assert_eq!(
1011 capabilities.schema_metadata(),
1012 SchemaMetadataCapability::LiveRebuiltMetadata,
1013 );
1014 assert_eq!(
1015 capabilities.relation_source(),
1016 RelationSourceCapability::LiveSource,
1017 );
1018 assert_eq!(
1019 capabilities.relation_target(),
1020 RelationTargetCapability::VolatileTarget,
1021 );
1022 assert_eq!(
1023 capabilities.live_validation(),
1024 LiveValidationCapability::Supported,
1025 );
1026 assert!(!capabilities.has_allocation_identity());
1027 assert!(!capabilities.participates_in_durable_commit());
1028 assert!(capabilities.is_volatile());
1029 }
1030
1031 #[test]
1032 fn store_owns_explicit_journaled_storage_config() {
1033 insert_canister("store_journaled_config", "Canister");
1034 let store = Store::new_journaled(
1035 Def::new("store_journaled_config", "Store"),
1036 "store_journaled_config::Canister",
1037 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1038 );
1039
1040 assert!(store.is_journaled_storage());
1041 assert!(!store.is_heap_storage());
1042 let journaled = store
1043 .journaled_memory_config()
1044 .expect("journaled model stores four-role config explicitly");
1045
1046 assert_eq!(journaled.data_memory_id(), 110);
1047 assert_eq!(journaled.index_memory_id(), 111);
1048 assert_eq!(journaled.schema_memory_id(), 112);
1049 assert_eq!(journaled.journal_memory_id(), 113);
1050 assert_eq!(store.stable_data_memory_id(), 110);
1051 assert_eq!(store.stable_index_memory_id(), 111);
1052 assert_eq!(store.stable_schema_memory_id(), 112);
1053 assert_eq!(store.journal_memory_id(), 113);
1054 assert!(store.validate().is_ok());
1055 }
1056
1057 #[test]
1058 fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
1059 let store = Store::new_journaled(
1060 Def::new("store_journaled_capabilities", "Store"),
1061 "store_journaled_capabilities::Canister",
1062 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1063 );
1064 let capabilities = store.storage_capabilities();
1065
1066 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1067 assert_eq!(
1068 capabilities.allocation_identity(),
1069 AllocationIdentityCapability::Present,
1070 );
1071 assert_eq!(capabilities.durability(), StoreDurability::Durable);
1072 assert_eq!(
1073 capabilities.recovery(),
1074 StoreRecoveryCapability::StableBasePlusJournalReplay,
1075 );
1076 assert_eq!(
1077 capabilities.commit_participation(),
1078 CommitParticipation::Durable,
1079 );
1080 assert_eq!(
1081 capabilities.schema_metadata(),
1082 SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1083 );
1084 assert_eq!(
1085 capabilities.relation_source(),
1086 RelationSourceCapability::DurableSource,
1087 );
1088 assert_eq!(
1089 capabilities.relation_target(),
1090 RelationTargetCapability::DurableTarget,
1091 );
1092 assert_eq!(
1093 capabilities.live_validation(),
1094 LiveValidationCapability::Supported,
1095 );
1096 assert!(capabilities.has_allocation_identity());
1097 assert!(capabilities.participates_in_durable_commit());
1098 assert!(!capabilities.is_volatile());
1099 }
1100
1101 #[test]
1102 fn journaled_store_allocations_use_role_named_stable_keys() {
1103 let store = Store::new_journaled(
1104 Def::new("demo::rpg", "CharacterStore"),
1105 "demo::rpg::Canister",
1106 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1107 );
1108
1109 assert_eq!(
1110 store.stable_data_allocation("demo_rpg").stable_key(),
1111 "icydb.demo_rpg.memory_110.data.v1",
1112 );
1113 assert_eq!(
1114 store.stable_index_allocation("demo_rpg").stable_key(),
1115 "icydb.demo_rpg.memory_111.index.v1",
1116 );
1117 assert_eq!(
1118 store.stable_schema_allocation("demo_rpg").stable_key(),
1119 "icydb.demo_rpg.memory_112.schema.v1",
1120 );
1121 assert_eq!(
1122 store.journal_allocation("demo_rpg").stable_key(),
1123 "icydb.demo_rpg.memory_113.journal.v1",
1124 );
1125 }
1126
1127 #[test]
1128 fn storage_capabilities_are_not_allocation_identity() {
1129 let store_a = Store::new_journaled(
1130 Def::new("demo::rpg", "CharacterStore"),
1131 "demo::rpg::Canister",
1132 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1133 );
1134 let store_b = Store::new_journaled(
1135 Def::new("demo::rpg", "InventoryStore"),
1136 "demo::rpg::Canister",
1137 StoreJournaledMemoryConfig::new(120, 121, 122, 123),
1138 );
1139
1140 assert_eq!(
1141 store_a.storage_capabilities(),
1142 store_b.storage_capabilities()
1143 );
1144 assert_ne!(
1145 store_a.stable_data_allocation("demo_rpg"),
1146 store_b.stable_data_allocation("demo_rpg"),
1147 "stable allocation identity must remain separate from capabilities",
1148 );
1149 }
1150
1151 #[test]
1152 fn capability_consumers_use_axes_not_storage_mode() {
1153 const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1154 match capabilities.commit_participation() {
1155 CommitParticipation::Durable => "durable",
1156 CommitParticipation::LiveOnly => "live-only",
1157 }
1158 }
1159
1160 let future_durable_heap_mode = StoreStorageCapabilities {
1161 storage_mode: StoreStorageMode::Heap,
1162 allocation_identity: AllocationIdentityCapability::Present,
1163 durability: StoreDurability::Durable,
1164 recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1165 commit_participation: CommitParticipation::Durable,
1166 schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1167 relation_source: RelationSourceCapability::DurableSource,
1168 relation_target: RelationTargetCapability::DurableTarget,
1169 live_validation: LiveValidationCapability::Supported,
1170 };
1171
1172 assert_eq!(commit_label(future_durable_heap_mode), "durable");
1173 assert!(future_durable_heap_mode.participates_in_durable_commit());
1174 assert_eq!(
1175 future_durable_heap_mode.storage_mode(),
1176 StoreStorageMode::Heap,
1177 "the diagnostic storage mode must not drive commit policy",
1178 );
1179 }
1180
1181 #[test]
1182 fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1183 insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1184 let store = Store::new_journaled(
1185 Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1186 "store_duplicate_journaled_role_memory_ids::Canister",
1187 StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1188 );
1189
1190 let err = store
1191 .validate()
1192 .expect_err("duplicate journaled role memory IDs must fail validation");
1193 let rendered = err.to_string();
1194
1195 assert!(
1196 rendered.contains("schema_memory_id and journal_memory_id must differ"),
1197 "expected duplicate journaled role memory-id error, got: {rendered}"
1198 );
1199 }
1200}