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]
422 pub const fn stable_data_memory_id(&self) -> u8 {
423 match self.storage {
424 StoreStorage::Journaled(config) => config.data_memory_id(),
425 StoreStorage::Heap(_) => panic!("heap stores do not have a stable data memory id"),
426 }
427 }
428
429 #[must_use]
430 pub const fn stable_index_memory_id(&self) -> u8 {
431 match self.storage {
432 StoreStorage::Journaled(config) => config.index_memory_id(),
433 StoreStorage::Heap(_) => panic!("heap stores do not have a stable index memory id"),
434 }
435 }
436
437 #[must_use]
438 pub const fn stable_schema_memory_id(&self) -> u8 {
439 match self.storage {
440 StoreStorage::Journaled(config) => config.schema_memory_id(),
441 StoreStorage::Heap(_) => panic!("heap stores do not have a stable schema memory id"),
442 }
443 }
444
445 #[must_use]
446 pub const fn journal_memory_id(&self) -> u8 {
447 match self.storage {
448 StoreStorage::Journaled(config) => config.journal_memory_id(),
449 StoreStorage::Heap(_) => panic!("heap stores do not have a journal memory id"),
450 }
451 }
452
453 #[must_use]
454 pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
455 self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
456 }
457
458 #[must_use]
461 pub fn stable_data_allocation_with_schema_metadata(
462 &self,
463 memory_namespace: &str,
464 schema_metadata: StableMemoryAllocationMetadata,
465 ) -> StableMemoryAllocation {
466 self.stable_allocation_with_schema_metadata(
467 memory_namespace,
468 StoreMemoryRole::Data,
469 schema_metadata,
470 )
471 }
472
473 #[must_use]
474 pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
475 self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
476 }
477
478 #[must_use]
481 pub fn stable_index_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::Index,
489 schema_metadata,
490 )
491 }
492
493 #[must_use]
494 pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
495 self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
496 }
497
498 #[must_use]
500 pub fn journal_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
501 StableMemoryAllocation::without_schema_metadata(
502 self.journal_memory_id(),
503 stable_memory_key(memory_namespace, self.store_name(), "journal"),
504 )
505 }
506
507 #[must_use]
510 pub fn stable_schema_allocation_with_schema_metadata(
511 &self,
512 memory_namespace: &str,
513 schema_metadata: StableMemoryAllocationMetadata,
514 ) -> StableMemoryAllocation {
515 self.stable_allocation_with_schema_metadata(
516 memory_namespace,
517 StoreMemoryRole::Schema,
518 schema_metadata,
519 )
520 }
521
522 #[must_use]
523 pub fn stable_allocation(
524 &self,
525 memory_namespace: &str,
526 role: StoreMemoryRole,
527 ) -> StableMemoryAllocation {
528 let memory_id = match role {
529 StoreMemoryRole::Data => self.stable_data_memory_id(),
530 StoreMemoryRole::Index => self.stable_index_memory_id(),
531 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
532 };
533
534 StableMemoryAllocation::without_schema_metadata(
535 memory_id,
536 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
537 )
538 }
539
540 fn stable_allocation_with_schema_metadata(
541 &self,
542 memory_namespace: &str,
543 role: StoreMemoryRole,
544 schema_metadata: StableMemoryAllocationMetadata,
545 ) -> StableMemoryAllocation {
546 let memory_id = match role {
547 StoreMemoryRole::Data => self.stable_data_memory_id(),
548 StoreMemoryRole::Index => self.stable_index_memory_id(),
549 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
550 };
551
552 StableMemoryAllocation::with_schema_metadata(
553 memory_id,
554 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
555 schema_metadata,
556 )
557 }
558}
559
560#[derive(Clone, Copy, Debug, Eq, PartialEq)]
561pub enum StoreMemoryRole {
562 Data,
563 Index,
564 Schema,
565}
566
567impl StoreMemoryRole {
568 #[must_use]
569 pub const fn as_str(self) -> &'static str {
570 match self {
571 Self::Data => "data",
572 Self::Index => "index",
573 Self::Schema => "schema",
574 }
575 }
576}
577
578#[derive(Clone, Debug, Eq, PartialEq)]
583pub struct StableMemoryAllocationMetadata {
584 version: Option<u32>,
585 fingerprint_method_version: Option<u8>,
586 fingerprint: Option<String>,
587}
588
589impl StableMemoryAllocationMetadata {
590 const fn new(
591 schema_version: Option<u32>,
592 schema_fingerprint_method_version: Option<u8>,
593 schema_fingerprint: Option<String>,
594 ) -> Self {
595 Self {
596 version: schema_version,
597 fingerprint_method_version: schema_fingerprint_method_version,
598 fingerprint: schema_fingerprint,
599 }
600 }
601
602 #[must_use]
604 pub const fn from_accepted_schema_contract(
605 schema_version: u32,
606 schema_fingerprint_method_version: u8,
607 schema_fingerprint: String,
608 ) -> Self {
609 Self::new(
610 Some(schema_version),
611 Some(schema_fingerprint_method_version),
612 Some(schema_fingerprint),
613 )
614 }
615
616 #[must_use]
619 pub const fn absent() -> Self {
620 Self::new(None, None, None)
621 }
622
623 #[must_use]
625 pub const fn schema_version(&self) -> Option<u32> {
626 self.version
627 }
628
629 #[must_use]
631 pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
632 self.fingerprint_method_version
633 }
634
635 #[must_use]
637 pub const fn schema_fingerprint(&self) -> Option<&str> {
638 match &self.fingerprint {
639 Some(value) => Some(value.as_str()),
640 None => None,
641 }
642 }
643}
644
645#[derive(Clone, Debug, Eq, PartialEq)]
651pub struct StableMemoryAllocation {
652 memory_id: u8,
653 stable_key: String,
654 schema_metadata: StableMemoryAllocationMetadata,
655}
656
657impl StableMemoryAllocation {
658 #[must_use]
660 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
661 Self::with_schema_metadata(
662 memory_id,
663 stable_key,
664 StableMemoryAllocationMetadata::absent(),
665 )
666 }
667
668 #[must_use]
673 pub const fn with_schema_metadata(
674 memory_id: u8,
675 stable_key: String,
676 schema_metadata: StableMemoryAllocationMetadata,
677 ) -> Self {
678 Self {
679 memory_id,
680 stable_key,
681 schema_metadata,
682 }
683 }
684
685 #[must_use]
687 pub const fn memory_id(&self) -> u8 {
688 self.memory_id
689 }
690
691 #[must_use]
693 pub const fn stable_key(&self) -> &str {
694 self.stable_key.as_str()
695 }
696
697 #[must_use]
699 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
700 &self.schema_metadata
701 }
702
703 #[must_use]
705 pub const fn schema_version(&self) -> Option<u32> {
706 self.schema_metadata.schema_version()
707 }
708
709 #[must_use]
711 pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
712 self.schema_metadata.schema_fingerprint_method_version()
713 }
714
715 #[must_use]
717 pub const fn schema_fingerprint(&self) -> Option<&str> {
718 self.schema_metadata.schema_fingerprint()
719 }
720
721 #[must_use]
726 pub fn same_identity_as(&self, other: &Self) -> bool {
727 self.memory_id == other.memory_id && self.stable_key == other.stable_key
728 }
729}
730
731#[must_use]
732pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
733 format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
734}
735
736impl MacroNode for Store {
737 fn as_any(&self) -> &dyn std::any::Any {
738 self
739 }
740}
741
742impl ValidateNode for Store {
743 fn validate(&self) -> Result<(), ErrorTree> {
744 let mut errs = ErrorTree::new();
745 let schema = schema_read();
746
747 match schema.cast_node::<Canister>(self.canister()) {
748 Ok(canister) => {
749 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
750 match self.storage() {
751 StoreStorage::Heap(_) => {}
752 StoreStorage::Journaled(config) => {
753 validate_journaled_memory_config(&mut errs, self, *config, canister);
754 }
755 }
756 }
757 Err(e) => errs.add(e),
758 }
759
760 errs.result()
761 }
762}
763
764fn validate_journaled_memory_config(
765 errs: &mut ErrorTree,
766 store: &Store,
767 config: StoreJournaledMemoryConfig,
768 canister: &Canister,
769) {
770 validate_stable_memory_role(
771 errs,
772 "data_memory_id",
773 "data stable key",
774 config.data_memory_id(),
775 store
776 .stable_data_allocation(canister.memory_namespace())
777 .stable_key(),
778 canister,
779 );
780 validate_stable_memory_role(
781 errs,
782 "index_memory_id",
783 "index stable key",
784 config.index_memory_id(),
785 store
786 .stable_index_allocation(canister.memory_namespace())
787 .stable_key(),
788 canister,
789 );
790 validate_stable_memory_role(
791 errs,
792 "schema_memory_id",
793 "schema stable key",
794 config.schema_memory_id(),
795 store
796 .stable_schema_allocation(canister.memory_namespace())
797 .stable_key(),
798 canister,
799 );
800 validate_stable_memory_role(
801 errs,
802 "journal_memory_id",
803 "journal stable key",
804 config.journal_memory_id(),
805 store
806 .journal_allocation(canister.memory_namespace())
807 .stable_key(),
808 canister,
809 );
810
811 validate_distinct_journaled_memory_ids(errs, config);
812}
813
814fn validate_distinct_journaled_memory_ids(
815 errs: &mut ErrorTree,
816 config: StoreJournaledMemoryConfig,
817) {
818 let roles = [
819 ("data_memory_id", config.data_memory_id()),
820 ("index_memory_id", config.index_memory_id()),
821 ("schema_memory_id", config.schema_memory_id()),
822 ("journal_memory_id", config.journal_memory_id()),
823 ];
824
825 for (idx, (left_label, left_id)) in roles.iter().enumerate() {
826 for (right_label, right_id) in roles.iter().skip(idx + 1) {
827 if left_id == right_id {
828 err!(
829 errs,
830 "{} and {} must differ (both are {})",
831 left_label,
832 right_label,
833 left_id,
834 );
835 }
836 }
837 }
838}
839
840fn validate_stable_memory_role(
841 errs: &mut ErrorTree,
842 memory_label: &str,
843 stable_key_label: &str,
844 memory_id: u8,
845 stable_key: &str,
846 canister: &Canister,
847) {
848 validate_memory_id_in_range(
849 errs,
850 memory_label,
851 memory_id,
852 canister.memory_min(),
853 canister.memory_max(),
854 );
855 validate_app_memory_id(errs, memory_label, memory_id);
856 validate_memory_id_not_reserved(errs, memory_label, memory_id);
857 validate_stable_key(errs, stable_key_label, stable_key);
858}
859
860impl VisitableNode for Store {
861 fn route_key(&self) -> String {
862 self.def().path()
863 }
864
865 fn drive<V: Visitor>(&self, v: &mut V) {
866 self.def().accept(v);
867 }
868}
869
870#[cfg(test)]
871mod tests {
872 use crate::{
873 build::schema_write,
874 node::{Canister, SchemaNode},
875 };
876
877 use super::*;
878
879 fn insert_canister(path_module: &'static str, ident: &'static str) {
880 schema_write().insert_node(SchemaNode::Canister(Canister::new(
881 Def::new(path_module, ident),
882 "test_db",
883 100,
884 254,
885 254,
886 )));
887 }
888
889 #[test]
890 fn store_allocations_default_to_absent_schema_metadata() {
891 let store = Store::new_journaled(
892 Def::new("demo::rpg", "CharacterStore"),
893 "CHARACTER_STORE",
894 "characters",
895 "demo::rpg::Canister",
896 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
897 );
898
899 for allocation in [
900 store.stable_data_allocation("demo_rpg"),
901 store.stable_index_allocation("demo_rpg"),
902 store.stable_schema_allocation("demo_rpg"),
903 store.journal_allocation("demo_rpg"),
904 ] {
905 assert_eq!(allocation.schema_version(), None);
906 assert_eq!(allocation.schema_fingerprint_method_version(), None);
907 assert_eq!(allocation.schema_fingerprint(), None);
908 assert_eq!(
909 allocation.schema_metadata(),
910 &StableMemoryAllocationMetadata::absent()
911 );
912 }
913 }
914
915 #[test]
916 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
917 let store = Store::new_journaled(
918 Def::new("demo::rpg", "CharacterStore"),
919 "CHARACTER_STORE",
920 "characters",
921 "demo::rpg::Canister",
922 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
923 );
924 let data = store.stable_data_allocation_with_schema_metadata(
925 "demo_rpg",
926 StableMemoryAllocationMetadata::from_accepted_schema_contract(
927 7,
928 2,
929 "data-row-layout".to_string(),
930 ),
931 );
932 let index = store.stable_index_allocation_with_schema_metadata(
933 "demo_rpg",
934 StableMemoryAllocationMetadata::from_accepted_schema_contract(
935 8,
936 3,
937 "index-catalog".to_string(),
938 ),
939 );
940 let schema = store.stable_schema_allocation_with_schema_metadata(
941 "demo_rpg",
942 StableMemoryAllocationMetadata::from_accepted_schema_contract(
943 10,
944 1,
945 "schema-catalog".to_string(),
946 ),
947 );
948 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
949 "demo_rpg",
950 StableMemoryAllocationMetadata::from_accepted_schema_contract(
951 9,
952 2,
953 "data-row-layout-v2".to_string(),
954 ),
955 );
956
957 assert_eq!(data.schema_version(), Some(7));
958 assert_eq!(data.schema_fingerprint_method_version(), Some(2));
959 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
960 assert_eq!(index.schema_version(), Some(8));
961 assert_eq!(index.schema_fingerprint_method_version(), Some(3));
962 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
963 assert_eq!(schema.schema_version(), Some(10));
964 assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
965 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
966 assert!(data.same_identity_as(&data_after_reconcile));
967 assert!(!data.same_identity_as(&index));
968 assert!(!data.same_identity_as(&schema));
969 }
970
971 #[test]
972 fn store_owns_explicit_heap_storage_config() {
973 insert_canister("store_heap_config", "Canister");
974 let store = Store::new_heap(
975 Def::new("store_heap_config", "Store"),
976 "STORE",
977 "heap_store",
978 "store_heap_config::Canister",
979 StoreHeapConfig::new(),
980 );
981
982 assert!(store.is_heap_storage());
983 assert!(store.validate().is_ok());
984 }
985
986 #[test]
987 fn heap_store_storage_capabilities_describe_volatile_contract() {
988 let store = Store::new_heap(
989 Def::new("store_heap_capabilities", "Store"),
990 "STORE",
991 "heap_store",
992 "store_heap_capabilities::Canister",
993 StoreHeapConfig::new(),
994 );
995 let capabilities = store.storage_capabilities();
996
997 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
998 assert_eq!(
999 capabilities.allocation_identity(),
1000 AllocationIdentityCapability::Absent,
1001 );
1002 assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1003 assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1004 assert_eq!(
1005 capabilities.commit_participation(),
1006 CommitParticipation::LiveOnly,
1007 );
1008 assert_eq!(
1009 capabilities.schema_metadata(),
1010 SchemaMetadataCapability::LiveRebuiltMetadata,
1011 );
1012 assert_eq!(
1013 capabilities.relation_source(),
1014 RelationSourceCapability::LiveSource,
1015 );
1016 assert_eq!(
1017 capabilities.relation_target(),
1018 RelationTargetCapability::VolatileTarget,
1019 );
1020 assert_eq!(
1021 capabilities.live_validation(),
1022 LiveValidationCapability::Supported,
1023 );
1024 assert!(!capabilities.has_allocation_identity());
1025 assert!(!capabilities.participates_in_durable_commit());
1026 assert!(capabilities.is_volatile());
1027 }
1028
1029 #[test]
1030 fn store_owns_explicit_journaled_storage_config() {
1031 insert_canister("store_journaled_config", "Canister");
1032 let store = Store::new_journaled(
1033 Def::new("store_journaled_config", "Store"),
1034 "STORE",
1035 "journaled_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",
1062 "journaled_store",
1063 "store_journaled_capabilities::Canister",
1064 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1065 );
1066 let capabilities = store.storage_capabilities();
1067
1068 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1069 assert_eq!(
1070 capabilities.allocation_identity(),
1071 AllocationIdentityCapability::Present,
1072 );
1073 assert_eq!(capabilities.durability(), StoreDurability::Durable);
1074 assert_eq!(
1075 capabilities.recovery(),
1076 StoreRecoveryCapability::StableBasePlusJournalReplay,
1077 );
1078 assert_eq!(
1079 capabilities.commit_participation(),
1080 CommitParticipation::Durable,
1081 );
1082 assert_eq!(
1083 capabilities.schema_metadata(),
1084 SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1085 );
1086 assert_eq!(
1087 capabilities.relation_source(),
1088 RelationSourceCapability::DurableSource,
1089 );
1090 assert_eq!(
1091 capabilities.relation_target(),
1092 RelationTargetCapability::DurableTarget,
1093 );
1094 assert_eq!(
1095 capabilities.live_validation(),
1096 LiveValidationCapability::Supported,
1097 );
1098 assert!(capabilities.has_allocation_identity());
1099 assert!(capabilities.participates_in_durable_commit());
1100 assert!(!capabilities.is_volatile());
1101 }
1102
1103 #[test]
1104 fn journaled_store_allocations_use_role_named_stable_keys() {
1105 let store = Store::new_journaled(
1106 Def::new("demo::rpg", "CharacterStore"),
1107 "CHARACTER_STORE",
1108 "characters",
1109 "demo::rpg::Canister",
1110 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1111 );
1112
1113 assert_eq!(
1114 store.stable_data_allocation("demo_rpg").stable_key(),
1115 "icydb.demo_rpg.characters.data.v1",
1116 );
1117 assert_eq!(
1118 store.stable_index_allocation("demo_rpg").stable_key(),
1119 "icydb.demo_rpg.characters.index.v1",
1120 );
1121 assert_eq!(
1122 store.stable_schema_allocation("demo_rpg").stable_key(),
1123 "icydb.demo_rpg.characters.schema.v1",
1124 );
1125 assert_eq!(
1126 store.journal_allocation("demo_rpg").stable_key(),
1127 "icydb.demo_rpg.characters.journal.v1",
1128 );
1129 }
1130
1131 #[test]
1132 fn storage_capabilities_are_not_allocation_identity() {
1133 let store_a = Store::new_journaled(
1134 Def::new("demo::rpg", "CharacterStore"),
1135 "CHARACTER_STORE",
1136 "characters",
1137 "demo::rpg::Canister",
1138 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1139 );
1140 let store_b = Store::new_journaled(
1141 Def::new("demo::rpg", "InventoryStore"),
1142 "INVENTORY_STORE",
1143 "inventory",
1144 "demo::rpg::Canister",
1145 StoreJournaledMemoryConfig::new(120, 121, 122, 123),
1146 );
1147
1148 assert_eq!(
1149 store_a.storage_capabilities(),
1150 store_b.storage_capabilities()
1151 );
1152 assert_ne!(
1153 store_a.stable_data_allocation("demo_rpg"),
1154 store_b.stable_data_allocation("demo_rpg"),
1155 "stable allocation identity must remain separate from capabilities",
1156 );
1157 }
1158
1159 #[test]
1160 fn capability_consumers_use_axes_not_storage_mode() {
1161 const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1162 match capabilities.commit_participation() {
1163 CommitParticipation::Durable => "durable",
1164 CommitParticipation::LiveOnly => "live-only",
1165 }
1166 }
1167
1168 let future_durable_heap_mode = StoreStorageCapabilities {
1169 storage_mode: StoreStorageMode::Heap,
1170 allocation_identity: AllocationIdentityCapability::Present,
1171 durability: StoreDurability::Durable,
1172 recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1173 commit_participation: CommitParticipation::Durable,
1174 schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1175 relation_source: RelationSourceCapability::DurableSource,
1176 relation_target: RelationTargetCapability::DurableTarget,
1177 live_validation: LiveValidationCapability::Supported,
1178 };
1179
1180 assert_eq!(commit_label(future_durable_heap_mode), "durable");
1181 assert!(future_durable_heap_mode.participates_in_durable_commit());
1182 assert_eq!(
1183 future_durable_heap_mode.storage_mode(),
1184 StoreStorageMode::Heap,
1185 "the diagnostic storage mode must not drive commit policy",
1186 );
1187 }
1188
1189 #[test]
1190 fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1191 insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1192 let store = Store::new_journaled(
1193 Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1194 "STORE",
1195 "duplicate_journaled_role_memory_ids",
1196 "store_duplicate_journaled_role_memory_ids::Canister",
1197 StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1198 );
1199
1200 let err = store
1201 .validate()
1202 .expect_err("duplicate journaled role memory IDs must fail validation");
1203 let rendered = err.to_string();
1204
1205 assert!(
1206 rendered.contains("schema_memory_id and journal_memory_id must differ"),
1207 "expected duplicate journaled role memory-id error, got: {rendered}"
1208 );
1209 }
1210}