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