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