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
771 {
772 let schema = schema_read();
773
774 match schema.cast_node::<Canister>(self.canister()) {
775 Ok(canister) => {
776 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
777 match self.storage() {
778 StoreStorage::Heap(_) => {}
779 StoreStorage::Journaled(config) => {
780 validate_journaled_memory_config(&mut errs, self, *config, canister);
781 }
782 }
783 }
784 Err(e) => errs.add(e),
785 }
786 }
787
788 errs.result()
789 }
790}
791
792fn validate_journaled_memory_config(
793 errs: &mut ErrorTree,
794 store: &Store,
795 config: StoreJournaledMemoryConfig,
796 canister: &Canister,
797) {
798 validate_stable_memory_role(
799 errs,
800 "data_memory_id",
801 "data stable key",
802 config.data_memory_id(),
803 store
804 .stable_data_allocation(canister.memory_namespace())
805 .stable_key(),
806 canister,
807 );
808 validate_stable_memory_role(
809 errs,
810 "index_memory_id",
811 "index stable key",
812 config.index_memory_id(),
813 store
814 .stable_index_allocation(canister.memory_namespace())
815 .stable_key(),
816 canister,
817 );
818 validate_stable_memory_role(
819 errs,
820 "schema_memory_id",
821 "schema stable key",
822 config.schema_memory_id(),
823 store
824 .stable_schema_allocation(canister.memory_namespace())
825 .stable_key(),
826 canister,
827 );
828 validate_stable_memory_role(
829 errs,
830 "journal_memory_id",
831 "journal stable key",
832 config.journal_memory_id(),
833 store
834 .journal_allocation(canister.memory_namespace())
835 .stable_key(),
836 canister,
837 );
838
839 validate_distinct_journaled_memory_ids(errs, config);
840}
841
842fn validate_distinct_journaled_memory_ids(
843 errs: &mut ErrorTree,
844 config: StoreJournaledMemoryConfig,
845) {
846 let roles = [
847 ("data_memory_id", config.data_memory_id()),
848 ("index_memory_id", config.index_memory_id()),
849 ("schema_memory_id", config.schema_memory_id()),
850 ("journal_memory_id", config.journal_memory_id()),
851 ];
852
853 for (idx, (left_label, left_id)) in roles.iter().enumerate() {
854 for (right_label, right_id) in roles.iter().skip(idx + 1) {
855 if left_id == right_id {
856 err!(
857 errs,
858 "{} and {} must differ (both are {})",
859 left_label,
860 right_label,
861 left_id,
862 );
863 }
864 }
865 }
866}
867
868fn validate_stable_memory_role(
869 errs: &mut ErrorTree,
870 memory_label: &str,
871 stable_key_label: &str,
872 memory_id: u8,
873 stable_key: &str,
874 canister: &Canister,
875) {
876 validate_memory_id_in_range(
877 errs,
878 memory_label,
879 memory_id,
880 canister.memory_min(),
881 canister.memory_max(),
882 );
883 validate_app_memory_id(errs, memory_label, memory_id);
884 validate_memory_id_not_reserved(errs, memory_label, memory_id);
885 validate_stable_key(errs, stable_key_label, stable_key);
886}
887
888impl VisitableNode for Store {
889 fn route_key(&self) -> String {
890 self.def().path()
891 }
892
893 fn drive<V: Visitor>(&self, v: &mut V) {
894 self.def().accept(v);
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use crate::{
901 build::schema_write,
902 node::{Canister, SchemaNode},
903 };
904
905 use super::*;
906
907 fn insert_canister(path_module: &'static str, ident: &'static str) {
908 schema_write().insert_node(SchemaNode::Canister(Canister::new(
909 Def::new(path_module, ident),
910 "test_db",
911 100,
912 254,
913 254,
914 )));
915 }
916
917 #[test]
918 fn store_allocations_default_to_absent_schema_metadata() {
919 let store = Store::new_journaled(
920 Def::new("demo::rpg", "CharacterStore"),
921 "CHARACTER_STORE",
922 "characters",
923 "demo::rpg::Canister",
924 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
925 );
926
927 for allocation in [
928 store.stable_data_allocation("demo_rpg"),
929 store.stable_index_allocation("demo_rpg"),
930 store.stable_schema_allocation("demo_rpg"),
931 store.journal_allocation("demo_rpg"),
932 ] {
933 assert_eq!(allocation.schema_version(), None);
934 assert_eq!(allocation.schema_fingerprint_method_version(), None);
935 assert_eq!(allocation.schema_fingerprint(), None);
936 assert_eq!(
937 allocation.schema_metadata(),
938 &StableMemoryAllocationMetadata::absent()
939 );
940 }
941 }
942
943 #[test]
944 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
945 let store = Store::new_journaled(
946 Def::new("demo::rpg", "CharacterStore"),
947 "CHARACTER_STORE",
948 "characters",
949 "demo::rpg::Canister",
950 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
951 );
952 let data = store.stable_data_allocation_with_schema_metadata(
953 "demo_rpg",
954 StableMemoryAllocationMetadata::from_accepted_schema_contract(
955 7,
956 2,
957 "data-row-layout".to_string(),
958 ),
959 );
960 let index = store.stable_index_allocation_with_schema_metadata(
961 "demo_rpg",
962 StableMemoryAllocationMetadata::from_accepted_schema_contract(
963 8,
964 3,
965 "index-catalog".to_string(),
966 ),
967 );
968 let schema = store.stable_schema_allocation_with_schema_metadata(
969 "demo_rpg",
970 StableMemoryAllocationMetadata::from_accepted_schema_contract(
971 10,
972 1,
973 "schema-catalog".to_string(),
974 ),
975 );
976 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
977 "demo_rpg",
978 StableMemoryAllocationMetadata::from_accepted_schema_contract(
979 9,
980 2,
981 "data-row-layout-v2".to_string(),
982 ),
983 );
984
985 assert_eq!(data.schema_version(), Some(7));
986 assert_eq!(data.schema_fingerprint_method_version(), Some(2));
987 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
988 assert_eq!(index.schema_version(), Some(8));
989 assert_eq!(index.schema_fingerprint_method_version(), Some(3));
990 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
991 assert_eq!(schema.schema_version(), Some(10));
992 assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
993 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
994 assert!(data.same_identity_as(&data_after_reconcile));
995 assert!(!data.same_identity_as(&index));
996 assert!(!data.same_identity_as(&schema));
997 }
998
999 #[test]
1000 fn store_owns_explicit_heap_storage_config() {
1001 insert_canister("store_heap_config", "Canister");
1002 let store = Store::new_heap(
1003 Def::new("store_heap_config", "Store"),
1004 "STORE",
1005 "heap_store",
1006 "store_heap_config::Canister",
1007 StoreHeapConfig::new(),
1008 );
1009
1010 assert!(store.is_heap_storage());
1011 assert!(store.validate().is_ok());
1012 }
1013
1014 #[test]
1015 fn heap_store_storage_capabilities_describe_volatile_contract() {
1016 let store = Store::new_heap(
1017 Def::new("store_heap_capabilities", "Store"),
1018 "STORE",
1019 "heap_store",
1020 "store_heap_capabilities::Canister",
1021 StoreHeapConfig::new(),
1022 );
1023 let capabilities = store.storage_capabilities();
1024
1025 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
1026 assert_eq!(
1027 capabilities.allocation_identity(),
1028 AllocationIdentityCapability::Absent,
1029 );
1030 assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1031 assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1032 assert_eq!(
1033 capabilities.commit_participation(),
1034 CommitParticipation::LiveOnly,
1035 );
1036 assert_eq!(
1037 capabilities.schema_metadata(),
1038 SchemaMetadataCapability::LiveRebuiltMetadata,
1039 );
1040 assert_eq!(
1041 capabilities.relation_source(),
1042 RelationSourceCapability::LiveSource,
1043 );
1044 assert_eq!(
1045 capabilities.relation_target(),
1046 RelationTargetCapability::VolatileTarget,
1047 );
1048 assert_eq!(
1049 capabilities.live_validation(),
1050 LiveValidationCapability::Supported,
1051 );
1052 assert!(!capabilities.has_allocation_identity());
1053 assert!(!capabilities.participates_in_durable_commit());
1054 assert!(capabilities.is_volatile());
1055 }
1056
1057 #[test]
1058 fn store_owns_explicit_journaled_storage_config() {
1059 insert_canister("store_journaled_config", "Canister");
1060 let store = Store::new_journaled(
1061 Def::new("store_journaled_config", "Store"),
1062 "STORE",
1063 "journaled_store",
1064 "store_journaled_config::Canister",
1065 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1066 );
1067
1068 assert!(store.is_journaled_storage());
1069 assert!(!store.is_heap_storage());
1070 let journaled = store
1071 .journaled_memory_config()
1072 .expect("journaled model stores four-role config explicitly");
1073
1074 assert_eq!(journaled.data_memory_id(), 110);
1075 assert_eq!(journaled.index_memory_id(), 111);
1076 assert_eq!(journaled.schema_memory_id(), 112);
1077 assert_eq!(journaled.journal_memory_id(), 113);
1078 assert_eq!(store.stable_data_memory_id(), 110);
1079 assert_eq!(store.stable_index_memory_id(), 111);
1080 assert_eq!(store.stable_schema_memory_id(), 112);
1081 assert_eq!(store.journal_memory_id(), 113);
1082 assert!(store.validate().is_ok());
1083 }
1084
1085 #[test]
1086 fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
1087 let store = Store::new_journaled(
1088 Def::new("store_journaled_capabilities", "Store"),
1089 "STORE",
1090 "journaled_store",
1091 "store_journaled_capabilities::Canister",
1092 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1093 );
1094 let capabilities = store.storage_capabilities();
1095
1096 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1097 assert_eq!(
1098 capabilities.allocation_identity(),
1099 AllocationIdentityCapability::Present,
1100 );
1101 assert_eq!(capabilities.durability(), StoreDurability::Durable);
1102 assert_eq!(
1103 capabilities.recovery(),
1104 StoreRecoveryCapability::StableBasePlusJournalReplay,
1105 );
1106 assert_eq!(
1107 capabilities.commit_participation(),
1108 CommitParticipation::Durable,
1109 );
1110 assert_eq!(
1111 capabilities.schema_metadata(),
1112 SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1113 );
1114 assert_eq!(
1115 capabilities.relation_source(),
1116 RelationSourceCapability::DurableSource,
1117 );
1118 assert_eq!(
1119 capabilities.relation_target(),
1120 RelationTargetCapability::DurableTarget,
1121 );
1122 assert_eq!(
1123 capabilities.live_validation(),
1124 LiveValidationCapability::Supported,
1125 );
1126 assert!(capabilities.has_allocation_identity());
1127 assert!(capabilities.participates_in_durable_commit());
1128 assert!(!capabilities.is_volatile());
1129 }
1130
1131 #[test]
1132 fn journaled_store_allocations_use_role_named_stable_keys() {
1133 let store = 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
1141 assert_eq!(
1142 store.stable_data_allocation("demo_rpg").stable_key(),
1143 "icydb.demo_rpg.characters.data.v1",
1144 );
1145 assert_eq!(
1146 store.stable_index_allocation("demo_rpg").stable_key(),
1147 "icydb.demo_rpg.characters.index.v1",
1148 );
1149 assert_eq!(
1150 store.stable_schema_allocation("demo_rpg").stable_key(),
1151 "icydb.demo_rpg.characters.schema.v1",
1152 );
1153 assert_eq!(
1154 store.journal_allocation("demo_rpg").stable_key(),
1155 "icydb.demo_rpg.characters.journal.v1",
1156 );
1157 }
1158
1159 #[test]
1160 fn storage_capabilities_are_not_allocation_identity() {
1161 let store_a = Store::new_journaled(
1162 Def::new("demo::rpg", "CharacterStore"),
1163 "CHARACTER_STORE",
1164 "characters",
1165 "demo::rpg::Canister",
1166 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1167 );
1168 let store_b = Store::new_journaled(
1169 Def::new("demo::rpg", "InventoryStore"),
1170 "INVENTORY_STORE",
1171 "inventory",
1172 "demo::rpg::Canister",
1173 StoreJournaledMemoryConfig::new(120, 121, 122, 123),
1174 );
1175
1176 assert_eq!(
1177 store_a.storage_capabilities(),
1178 store_b.storage_capabilities()
1179 );
1180 assert_ne!(
1181 store_a.stable_data_allocation("demo_rpg"),
1182 store_b.stable_data_allocation("demo_rpg"),
1183 "stable allocation identity must remain separate from capabilities",
1184 );
1185 }
1186
1187 #[test]
1188 fn capability_consumers_use_axes_not_storage_mode() {
1189 const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1190 match capabilities.commit_participation() {
1191 CommitParticipation::Durable => "durable",
1192 CommitParticipation::LiveOnly => "live-only",
1193 }
1194 }
1195
1196 let future_durable_heap_mode = StoreStorageCapabilities {
1197 storage_mode: StoreStorageMode::Heap,
1198 allocation_identity: AllocationIdentityCapability::Present,
1199 durability: StoreDurability::Durable,
1200 recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
1201 commit_participation: CommitParticipation::Durable,
1202 schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1203 relation_source: RelationSourceCapability::DurableSource,
1204 relation_target: RelationTargetCapability::DurableTarget,
1205 live_validation: LiveValidationCapability::Supported,
1206 };
1207
1208 assert_eq!(commit_label(future_durable_heap_mode), "durable");
1209 assert!(future_durable_heap_mode.participates_in_durable_commit());
1210 assert_eq!(
1211 future_durable_heap_mode.storage_mode(),
1212 StoreStorageMode::Heap,
1213 "the diagnostic storage mode must not drive commit policy",
1214 );
1215 }
1216
1217 #[test]
1218 fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1219 insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1220 let store = Store::new_journaled(
1221 Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1222 "STORE",
1223 "duplicate_journaled_role_memory_ids",
1224 "store_duplicate_journaled_role_memory_ids::Canister",
1225 StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1226 );
1227
1228 let err = store
1229 .validate()
1230 .expect_err("duplicate journaled role memory IDs must fail validation");
1231 let rendered = err.to_string();
1232
1233 assert!(
1234 rendered.contains("schema_memory_id and journal_memory_id must differ"),
1235 "expected duplicate journaled role memory-id error, got: {rendered}"
1236 );
1237 }
1238}