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