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