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