1use crate::node::{
2 validate_app_memory_id, validate_memory_id_in_range, validate_memory_id_not_reserved,
3 validate_stable_key, validate_stable_key_segment,
4};
5use crate::prelude::*;
6
7#[derive(Clone, Debug, Serialize)]
17pub struct Store {
18 def: Def,
19 ident: &'static str,
20 name: &'static str,
21 canister: &'static str,
22 storage: StoreStorage,
23}
24
25#[derive(Clone, Debug, Serialize)]
31pub enum StoreStorage {
32 Stable(StoreStableMemoryConfig),
35 Heap(StoreHeapConfig),
37 Journaled(StoreJournaledMemoryConfig),
40}
41
42impl StoreStorage {
43 #[must_use]
48 pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
49 match self {
50 Self::Stable(config) => Some(config),
51 Self::Heap(_) | Self::Journaled(_) => None,
52 }
53 }
54
55 #[must_use]
57 pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
58 match self {
59 Self::Journaled(config) => Some(config),
60 Self::Stable(_) | Self::Heap(_) => None,
61 }
62 }
63
64 #[must_use]
66 pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
67 match self {
68 Self::Stable(_) => StoreStorageCapabilities::stable(),
69 Self::Heap(_) => StoreStorageCapabilities::heap(),
70 Self::Journaled(_) => StoreStorageCapabilities::journaled(),
71 }
72 }
73}
74
75#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
79pub enum StoreStorageMode {
80 Stable,
82 Heap,
84 Journaled,
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
90pub enum AllocationIdentityCapability {
91 Present,
93 Absent,
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
99pub enum StoreDurability {
100 Durable,
102 Volatile,
104}
105
106#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
108pub enum StoreRecoveryCapability {
109 StableCommitReplay,
111 StableBasePlusJournalReplay,
114 None,
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
120pub enum CommitParticipation {
121 Durable,
123 LiveOnly,
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
129pub enum SchemaMetadataCapability {
130 DurableAcceptedHistory,
132 LiveRebuiltMetadata,
134 CanonicalStableHistoryPlusJournalTail,
136}
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
140pub enum RelationSourceCapability {
141 DurableSource,
143 LiveSource,
145}
146
147#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
149pub enum RelationTargetCapability {
150 DurableTarget,
152 VolatileTarget,
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
158pub enum LiveValidationCapability {
159 Supported,
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
169pub struct StoreStorageCapabilities {
170 storage_mode: StoreStorageMode,
171 allocation_identity: AllocationIdentityCapability,
172 durability: StoreDurability,
173 recovery: StoreRecoveryCapability,
174 commit_participation: CommitParticipation,
175 schema_metadata: SchemaMetadataCapability,
176 relation_source: RelationSourceCapability,
177 relation_target: RelationTargetCapability,
178 live_validation: LiveValidationCapability,
179}
180
181impl StoreStorageCapabilities {
182 #[must_use]
184 pub const fn stable() -> Self {
185 Self {
186 storage_mode: StoreStorageMode::Stable,
187 allocation_identity: AllocationIdentityCapability::Present,
188 durability: StoreDurability::Durable,
189 recovery: StoreRecoveryCapability::StableCommitReplay,
190 commit_participation: CommitParticipation::Durable,
191 schema_metadata: SchemaMetadataCapability::DurableAcceptedHistory,
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 heap() -> Self {
201 Self {
202 storage_mode: StoreStorageMode::Heap,
203 allocation_identity: AllocationIdentityCapability::Absent,
204 durability: StoreDurability::Volatile,
205 recovery: StoreRecoveryCapability::None,
206 commit_participation: CommitParticipation::LiveOnly,
207 schema_metadata: SchemaMetadataCapability::LiveRebuiltMetadata,
208 relation_source: RelationSourceCapability::LiveSource,
209 relation_target: RelationTargetCapability::VolatileTarget,
210 live_validation: LiveValidationCapability::Supported,
211 }
212 }
213
214 #[must_use]
216 pub const fn journaled() -> Self {
217 Self {
218 storage_mode: StoreStorageMode::Journaled,
219 allocation_identity: AllocationIdentityCapability::Present,
220 durability: StoreDurability::Durable,
221 recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
222 commit_participation: CommitParticipation::Durable,
223 schema_metadata: SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
224 relation_source: RelationSourceCapability::DurableSource,
225 relation_target: RelationTargetCapability::DurableTarget,
226 live_validation: LiveValidationCapability::Supported,
227 }
228 }
229
230 #[must_use]
232 pub const fn storage_mode(self) -> StoreStorageMode {
233 self.storage_mode
234 }
235
236 #[must_use]
238 pub const fn allocation_identity(self) -> AllocationIdentityCapability {
239 self.allocation_identity
240 }
241
242 #[must_use]
244 pub const fn durability(self) -> StoreDurability {
245 self.durability
246 }
247
248 #[must_use]
250 pub const fn recovery(self) -> StoreRecoveryCapability {
251 self.recovery
252 }
253
254 #[must_use]
256 pub const fn commit_participation(self) -> CommitParticipation {
257 self.commit_participation
258 }
259
260 #[must_use]
262 pub const fn schema_metadata(self) -> SchemaMetadataCapability {
263 self.schema_metadata
264 }
265
266 #[must_use]
268 pub const fn relation_source(self) -> RelationSourceCapability {
269 self.relation_source
270 }
271
272 #[must_use]
274 pub const fn relation_target(self) -> RelationTargetCapability {
275 self.relation_target
276 }
277
278 #[must_use]
280 pub const fn live_validation(self) -> LiveValidationCapability {
281 self.live_validation
282 }
283
284 #[must_use]
286 pub const fn has_allocation_identity(self) -> bool {
287 matches!(
288 self.allocation_identity,
289 AllocationIdentityCapability::Present
290 )
291 }
292
293 #[must_use]
295 pub const fn participates_in_durable_commit(self) -> bool {
296 matches!(self.commit_participation, CommitParticipation::Durable)
297 }
298
299 #[must_use]
301 pub const fn is_volatile(self) -> bool {
302 matches!(self.durability, StoreDurability::Volatile)
303 }
304}
305
306#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
308pub struct StoreHeapConfig;
309
310impl StoreHeapConfig {
311 #[must_use]
313 pub const fn new() -> Self {
314 Self
315 }
316}
317
318#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
320pub struct StoreStableMemoryConfig {
321 data: u8,
322 index: u8,
323 schema: u8,
324}
325
326#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
329pub struct StoreJournaledMemoryConfig {
330 data: u8,
331 index: u8,
332 schema: u8,
333 journal: u8,
334}
335
336impl StoreJournaledMemoryConfig {
337 #[must_use]
340 pub const fn new(
341 data_memory_id: u8,
342 index_memory_id: u8,
343 schema_memory_id: u8,
344 journal_memory_id: u8,
345 ) -> Self {
346 Self {
347 data: data_memory_id,
348 index: index_memory_id,
349 schema: schema_memory_id,
350 journal: journal_memory_id,
351 }
352 }
353
354 #[must_use]
356 pub const fn data_memory_id(self) -> u8 {
357 self.data
358 }
359
360 #[must_use]
362 pub const fn index_memory_id(self) -> u8 {
363 self.index
364 }
365
366 #[must_use]
368 pub const fn schema_memory_id(self) -> u8 {
369 self.schema
370 }
371
372 #[must_use]
374 pub const fn journal_memory_id(self) -> u8 {
375 self.journal
376 }
377}
378
379impl StoreStableMemoryConfig {
380 #[must_use]
383 pub const fn new(data_memory_id: u8, index_memory_id: u8, schema_memory_id: u8) -> Self {
384 Self {
385 data: data_memory_id,
386 index: index_memory_id,
387 schema: schema_memory_id,
388 }
389 }
390
391 #[must_use]
393 pub const fn data_memory_id(self) -> u8 {
394 self.data
395 }
396
397 #[must_use]
399 pub const fn index_memory_id(self) -> u8 {
400 self.index
401 }
402
403 #[must_use]
405 pub const fn schema_memory_id(self) -> u8 {
406 self.schema
407 }
408}
409
410impl Store {
411 #[must_use]
415 pub const fn new_stable(
416 def: Def,
417 ident: &'static str,
418 store_name: &'static str,
419 canister: &'static str,
420 stable: StoreStableMemoryConfig,
421 ) -> Self {
422 Self {
423 def,
424 ident,
425 name: store_name,
426 canister,
427 storage: StoreStorage::Stable(stable),
428 }
429 }
430
431 #[must_use]
433 pub const fn new_heap(
434 def: Def,
435 ident: &'static str,
436 store_name: &'static str,
437 canister: &'static str,
438 heap: StoreHeapConfig,
439 ) -> Self {
440 Self {
441 def,
442 ident,
443 name: store_name,
444 canister,
445 storage: StoreStorage::Heap(heap),
446 }
447 }
448
449 #[must_use]
451 pub const fn new_journaled(
452 def: Def,
453 ident: &'static str,
454 store_name: &'static str,
455 canister: &'static str,
456 journaled: StoreJournaledMemoryConfig,
457 ) -> Self {
458 Self {
459 def,
460 ident,
461 name: store_name,
462 canister,
463 storage: StoreStorage::Journaled(journaled),
464 }
465 }
466
467 #[must_use]
468 pub const fn def(&self) -> &Def {
469 &self.def
470 }
471
472 #[must_use]
473 pub const fn ident(&self) -> &'static str {
474 self.ident
475 }
476
477 #[must_use]
478 pub const fn store_name(&self) -> &'static str {
479 self.name
480 }
481
482 #[must_use]
483 pub const fn canister(&self) -> &'static str {
484 self.canister
485 }
486
487 #[must_use]
489 pub const fn storage(&self) -> &StoreStorage {
490 &self.storage
491 }
492
493 #[must_use]
495 pub const fn is_stable_storage(&self) -> bool {
496 matches!(self.storage, StoreStorage::Stable(_))
497 }
498
499 #[must_use]
501 pub const fn is_heap_storage(&self) -> bool {
502 matches!(self.storage, StoreStorage::Heap(_))
503 }
504
505 #[must_use]
507 pub const fn is_journaled_storage(&self) -> bool {
508 matches!(self.storage, StoreStorage::Journaled(_))
509 }
510
511 #[must_use]
513 pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
514 self.storage.stable_memory_config()
515 }
516
517 #[must_use]
520 pub const fn journaled_memory_config(&self) -> Option<&StoreJournaledMemoryConfig> {
521 self.storage.journaled_memory_config()
522 }
523
524 #[must_use]
526 pub const fn storage_capabilities(&self) -> StoreStorageCapabilities {
527 self.storage.storage_capabilities()
528 }
529
530 #[must_use]
531 pub const fn stable_data_memory_id(&self) -> u8 {
532 match self.storage {
533 StoreStorage::Stable(config) => config.data_memory_id(),
534 StoreStorage::Journaled(config) => config.data_memory_id(),
535 StoreStorage::Heap(_) => panic!("heap stores do not have a stable data memory id"),
536 }
537 }
538
539 #[must_use]
540 pub const fn stable_index_memory_id(&self) -> u8 {
541 match self.storage {
542 StoreStorage::Stable(config) => config.index_memory_id(),
543 StoreStorage::Journaled(config) => config.index_memory_id(),
544 StoreStorage::Heap(_) => panic!("heap stores do not have a stable index memory id"),
545 }
546 }
547
548 #[must_use]
549 pub const fn stable_schema_memory_id(&self) -> u8 {
550 match self.storage {
551 StoreStorage::Stable(config) => config.schema_memory_id(),
552 StoreStorage::Journaled(config) => config.schema_memory_id(),
553 StoreStorage::Heap(_) => panic!("heap stores do not have a stable schema memory id"),
554 }
555 }
556
557 #[must_use]
558 pub const fn journal_memory_id(&self) -> u8 {
559 match self.storage {
560 StoreStorage::Journaled(config) => config.journal_memory_id(),
561 StoreStorage::Stable(_) => {
562 panic!("stable stores do not have a journal memory id")
563 }
564 StoreStorage::Heap(_) => panic!("heap stores do not have a journal memory id"),
565 }
566 }
567
568 #[must_use]
569 pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
570 self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
571 }
572
573 #[must_use]
576 pub fn stable_data_allocation_with_schema_metadata(
577 &self,
578 memory_namespace: &str,
579 schema_metadata: StableMemoryAllocationMetadata,
580 ) -> StableMemoryAllocation {
581 self.stable_allocation_with_schema_metadata(
582 memory_namespace,
583 StoreMemoryRole::Data,
584 schema_metadata,
585 )
586 }
587
588 #[must_use]
589 pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
590 self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
591 }
592
593 #[must_use]
596 pub fn stable_index_allocation_with_schema_metadata(
597 &self,
598 memory_namespace: &str,
599 schema_metadata: StableMemoryAllocationMetadata,
600 ) -> StableMemoryAllocation {
601 self.stable_allocation_with_schema_metadata(
602 memory_namespace,
603 StoreMemoryRole::Index,
604 schema_metadata,
605 )
606 }
607
608 #[must_use]
609 pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
610 self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
611 }
612
613 #[must_use]
615 pub fn journal_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
616 StableMemoryAllocation::without_schema_metadata(
617 self.journal_memory_id(),
618 stable_memory_key(memory_namespace, self.store_name(), "journal"),
619 )
620 }
621
622 #[must_use]
625 pub fn stable_schema_allocation_with_schema_metadata(
626 &self,
627 memory_namespace: &str,
628 schema_metadata: StableMemoryAllocationMetadata,
629 ) -> StableMemoryAllocation {
630 self.stable_allocation_with_schema_metadata(
631 memory_namespace,
632 StoreMemoryRole::Schema,
633 schema_metadata,
634 )
635 }
636
637 #[must_use]
638 pub fn stable_allocation(
639 &self,
640 memory_namespace: &str,
641 role: StoreMemoryRole,
642 ) -> StableMemoryAllocation {
643 let memory_id = match role {
644 StoreMemoryRole::Data => self.stable_data_memory_id(),
645 StoreMemoryRole::Index => self.stable_index_memory_id(),
646 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
647 };
648
649 StableMemoryAllocation::without_schema_metadata(
650 memory_id,
651 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
652 )
653 }
654
655 fn stable_allocation_with_schema_metadata(
656 &self,
657 memory_namespace: &str,
658 role: StoreMemoryRole,
659 schema_metadata: StableMemoryAllocationMetadata,
660 ) -> StableMemoryAllocation {
661 let memory_id = match role {
662 StoreMemoryRole::Data => self.stable_data_memory_id(),
663 StoreMemoryRole::Index => self.stable_index_memory_id(),
664 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
665 };
666
667 StableMemoryAllocation::with_schema_metadata(
668 memory_id,
669 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
670 schema_metadata,
671 )
672 }
673}
674
675#[derive(Clone, Copy, Debug, Eq, PartialEq)]
676pub enum StoreMemoryRole {
677 Data,
678 Index,
679 Schema,
680}
681
682impl StoreMemoryRole {
683 #[must_use]
684 pub const fn as_str(self) -> &'static str {
685 match self {
686 Self::Data => "data",
687 Self::Index => "index",
688 Self::Schema => "schema",
689 }
690 }
691}
692
693#[derive(Clone, Debug, Eq, PartialEq)]
698pub struct StableMemoryAllocationMetadata {
699 version: Option<u32>,
700 fingerprint_method_version: Option<u8>,
701 fingerprint: Option<String>,
702}
703
704impl StableMemoryAllocationMetadata {
705 const fn new(
706 schema_version: Option<u32>,
707 schema_fingerprint_method_version: Option<u8>,
708 schema_fingerprint: Option<String>,
709 ) -> Self {
710 Self {
711 version: schema_version,
712 fingerprint_method_version: schema_fingerprint_method_version,
713 fingerprint: schema_fingerprint,
714 }
715 }
716
717 #[must_use]
719 pub const fn from_accepted_schema_contract(
720 schema_version: u32,
721 schema_fingerprint_method_version: u8,
722 schema_fingerprint: String,
723 ) -> Self {
724 Self::new(
725 Some(schema_version),
726 Some(schema_fingerprint_method_version),
727 Some(schema_fingerprint),
728 )
729 }
730
731 #[must_use]
734 pub const fn absent() -> Self {
735 Self::new(None, None, None)
736 }
737
738 #[must_use]
740 pub const fn schema_version(&self) -> Option<u32> {
741 self.version
742 }
743
744 #[must_use]
746 pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
747 self.fingerprint_method_version
748 }
749
750 #[must_use]
752 pub const fn schema_fingerprint(&self) -> Option<&str> {
753 match &self.fingerprint {
754 Some(value) => Some(value.as_str()),
755 None => None,
756 }
757 }
758}
759
760#[derive(Clone, Debug, Eq, PartialEq)]
766pub struct StableMemoryAllocation {
767 memory_id: u8,
768 stable_key: String,
769 schema_metadata: StableMemoryAllocationMetadata,
770}
771
772impl StableMemoryAllocation {
773 #[must_use]
775 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
776 Self::with_schema_metadata(
777 memory_id,
778 stable_key,
779 StableMemoryAllocationMetadata::absent(),
780 )
781 }
782
783 #[must_use]
788 pub const fn with_schema_metadata(
789 memory_id: u8,
790 stable_key: String,
791 schema_metadata: StableMemoryAllocationMetadata,
792 ) -> Self {
793 Self {
794 memory_id,
795 stable_key,
796 schema_metadata,
797 }
798 }
799
800 #[must_use]
802 pub const fn memory_id(&self) -> u8 {
803 self.memory_id
804 }
805
806 #[must_use]
808 pub const fn stable_key(&self) -> &str {
809 self.stable_key.as_str()
810 }
811
812 #[must_use]
814 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
815 &self.schema_metadata
816 }
817
818 #[must_use]
820 pub const fn schema_version(&self) -> Option<u32> {
821 self.schema_metadata.schema_version()
822 }
823
824 #[must_use]
826 pub const fn schema_fingerprint_method_version(&self) -> Option<u8> {
827 self.schema_metadata.schema_fingerprint_method_version()
828 }
829
830 #[must_use]
832 pub const fn schema_fingerprint(&self) -> Option<&str> {
833 self.schema_metadata.schema_fingerprint()
834 }
835
836 #[must_use]
841 pub fn same_identity_as(&self, other: &Self) -> bool {
842 self.memory_id == other.memory_id && self.stable_key == other.stable_key
843 }
844}
845
846#[must_use]
847pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
848 format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
849}
850
851impl MacroNode for Store {
852 fn as_any(&self) -> &dyn std::any::Any {
853 self
854 }
855}
856
857impl ValidateNode for Store {
858 fn validate(&self) -> Result<(), ErrorTree> {
859 let mut errs = ErrorTree::new();
860 let schema = schema_read();
861
862 match schema.cast_node::<Canister>(self.canister()) {
863 Ok(canister) => {
864 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
865 match self.storage() {
866 StoreStorage::Stable(config) => {
867 validate_stable_memory_config(&mut errs, self, *config, canister);
868 }
869 StoreStorage::Heap(_) => {}
870 StoreStorage::Journaled(config) => {
871 validate_journaled_memory_config(&mut errs, self, *config, canister);
872 }
873 }
874 }
875 Err(e) => errs.add(e),
876 }
877
878 errs.result()
879 }
880}
881
882fn validate_journaled_memory_config(
883 errs: &mut ErrorTree,
884 store: &Store,
885 config: StoreJournaledMemoryConfig,
886 canister: &Canister,
887) {
888 validate_stable_memory_role(
889 errs,
890 "data_memory_id",
891 "data stable key",
892 config.data_memory_id(),
893 store
894 .stable_data_allocation(canister.memory_namespace())
895 .stable_key(),
896 canister,
897 );
898 validate_stable_memory_role(
899 errs,
900 "index_memory_id",
901 "index stable key",
902 config.index_memory_id(),
903 store
904 .stable_index_allocation(canister.memory_namespace())
905 .stable_key(),
906 canister,
907 );
908 validate_stable_memory_role(
909 errs,
910 "schema_memory_id",
911 "schema stable key",
912 config.schema_memory_id(),
913 store
914 .stable_schema_allocation(canister.memory_namespace())
915 .stable_key(),
916 canister,
917 );
918 validate_stable_memory_role(
919 errs,
920 "journal_memory_id",
921 "journal stable key",
922 config.journal_memory_id(),
923 store
924 .journal_allocation(canister.memory_namespace())
925 .stable_key(),
926 canister,
927 );
928
929 validate_distinct_journaled_memory_ids(errs, config);
930}
931
932fn validate_distinct_journaled_memory_ids(
933 errs: &mut ErrorTree,
934 config: StoreJournaledMemoryConfig,
935) {
936 let roles = [
937 ("data_memory_id", config.data_memory_id()),
938 ("index_memory_id", config.index_memory_id()),
939 ("schema_memory_id", config.schema_memory_id()),
940 ("journal_memory_id", config.journal_memory_id()),
941 ];
942
943 for (idx, (left_label, left_id)) in roles.iter().enumerate() {
944 for (right_label, right_id) in roles.iter().skip(idx + 1) {
945 if left_id == right_id {
946 err!(
947 errs,
948 "{} and {} must differ (both are {})",
949 left_label,
950 right_label,
951 left_id,
952 );
953 }
954 }
955 }
956}
957
958fn validate_stable_memory_config(
959 errs: &mut ErrorTree,
960 store: &Store,
961 config: StoreStableMemoryConfig,
962 canister: &Canister,
963) {
964 validate_stable_memory_role(
965 errs,
966 "data_memory_id",
967 "data stable key",
968 config.data_memory_id(),
969 store
970 .stable_data_allocation(canister.memory_namespace())
971 .stable_key(),
972 canister,
973 );
974 validate_stable_memory_role(
975 errs,
976 "index_memory_id",
977 "index stable key",
978 config.index_memory_id(),
979 store
980 .stable_index_allocation(canister.memory_namespace())
981 .stable_key(),
982 canister,
983 );
984 validate_stable_memory_role(
985 errs,
986 "schema_memory_id",
987 "schema stable key",
988 config.schema_memory_id(),
989 store
990 .stable_schema_allocation(canister.memory_namespace())
991 .stable_key(),
992 canister,
993 );
994
995 if config.data_memory_id() == config.index_memory_id() {
996 err!(
997 errs,
998 "data_memory_id and index_memory_id must differ (both are {})",
999 config.data_memory_id(),
1000 );
1001 }
1002 if config.data_memory_id() == config.schema_memory_id() {
1003 err!(
1004 errs,
1005 "data_memory_id and schema_memory_id must differ (both are {})",
1006 config.data_memory_id(),
1007 );
1008 }
1009 if config.index_memory_id() == config.schema_memory_id() {
1010 err!(
1011 errs,
1012 "index_memory_id and schema_memory_id must differ (both are {})",
1013 config.index_memory_id(),
1014 );
1015 }
1016}
1017
1018fn validate_stable_memory_role(
1019 errs: &mut ErrorTree,
1020 memory_label: &str,
1021 stable_key_label: &str,
1022 memory_id: u8,
1023 stable_key: &str,
1024 canister: &Canister,
1025) {
1026 validate_memory_id_in_range(
1027 errs,
1028 memory_label,
1029 memory_id,
1030 canister.memory_min(),
1031 canister.memory_max(),
1032 );
1033 validate_app_memory_id(errs, memory_label, memory_id);
1034 validate_memory_id_not_reserved(errs, memory_label, memory_id);
1035 validate_stable_key(errs, stable_key_label, stable_key);
1036}
1037
1038impl VisitableNode for Store {
1039 fn route_key(&self) -> String {
1040 self.def().path()
1041 }
1042
1043 fn drive<V: Visitor>(&self, v: &mut V) {
1044 self.def().accept(v);
1045 }
1046}
1047
1048#[cfg(test)]
1049mod tests {
1050 use crate::{
1051 build::schema_write,
1052 node::{Canister, SchemaNode},
1053 };
1054
1055 use super::*;
1056
1057 fn insert_canister(path_module: &'static str, ident: &'static str) {
1058 schema_write().insert_node(SchemaNode::Canister(Canister::new(
1059 Def::new(path_module, ident),
1060 "test_db",
1061 100,
1062 254,
1063 254,
1064 )));
1065 }
1066
1067 #[test]
1068 fn store_stable_keys_use_durable_icydb_shape() {
1069 let store = Store::new_stable(
1070 Def::new("demo::rpg", "CharacterStore"),
1071 "CHARACTER_STORE",
1072 "characters",
1073 "demo::rpg::Canister",
1074 StoreStableMemoryConfig::new(110, 111, 112),
1075 );
1076
1077 assert_eq!(
1078 store.stable_data_allocation("demo_rpg").stable_key(),
1079 "icydb.demo_rpg.characters.data.v1",
1080 );
1081 assert_eq!(
1082 store.stable_index_allocation("demo_rpg").stable_key(),
1083 "icydb.demo_rpg.characters.index.v1",
1084 );
1085 assert_eq!(
1086 store.stable_schema_allocation("demo_rpg").stable_key(),
1087 "icydb.demo_rpg.characters.schema.v1",
1088 );
1089 }
1090
1091 #[test]
1092 fn store_allocations_default_to_absent_schema_metadata() {
1093 let store = Store::new_stable(
1094 Def::new("demo::rpg", "CharacterStore"),
1095 "CHARACTER_STORE",
1096 "characters",
1097 "demo::rpg::Canister",
1098 StoreStableMemoryConfig::new(110, 111, 112),
1099 );
1100
1101 for allocation in [
1102 store.stable_data_allocation("demo_rpg"),
1103 store.stable_index_allocation("demo_rpg"),
1104 store.stable_schema_allocation("demo_rpg"),
1105 ] {
1106 assert_eq!(allocation.schema_version(), None);
1107 assert_eq!(allocation.schema_fingerprint_method_version(), None);
1108 assert_eq!(allocation.schema_fingerprint(), None);
1109 assert_eq!(
1110 allocation.schema_metadata(),
1111 &StableMemoryAllocationMetadata::absent()
1112 );
1113 }
1114 }
1115
1116 #[test]
1117 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
1118 let store = Store::new_stable(
1119 Def::new("demo::rpg", "CharacterStore"),
1120 "CHARACTER_STORE",
1121 "characters",
1122 "demo::rpg::Canister",
1123 StoreStableMemoryConfig::new(110, 111, 112),
1124 );
1125 let data = store.stable_data_allocation_with_schema_metadata(
1126 "demo_rpg",
1127 StableMemoryAllocationMetadata::from_accepted_schema_contract(
1128 7,
1129 2,
1130 "data-row-layout".to_string(),
1131 ),
1132 );
1133 let index = store.stable_index_allocation_with_schema_metadata(
1134 "demo_rpg",
1135 StableMemoryAllocationMetadata::from_accepted_schema_contract(
1136 8,
1137 3,
1138 "index-catalog".to_string(),
1139 ),
1140 );
1141 let schema = store.stable_schema_allocation_with_schema_metadata(
1142 "demo_rpg",
1143 StableMemoryAllocationMetadata::from_accepted_schema_contract(
1144 10,
1145 1,
1146 "schema-catalog".to_string(),
1147 ),
1148 );
1149 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
1150 "demo_rpg",
1151 StableMemoryAllocationMetadata::from_accepted_schema_contract(
1152 9,
1153 2,
1154 "data-row-layout-v2".to_string(),
1155 ),
1156 );
1157
1158 assert_eq!(data.schema_version(), Some(7));
1159 assert_eq!(data.schema_fingerprint_method_version(), Some(2));
1160 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
1161 assert_eq!(index.schema_version(), Some(8));
1162 assert_eq!(index.schema_fingerprint_method_version(), Some(3));
1163 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
1164 assert_eq!(schema.schema_version(), Some(10));
1165 assert_eq!(schema.schema_fingerprint_method_version(), Some(1));
1166 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
1167 assert!(data.same_identity_as(&data_after_reconcile));
1168 assert!(!data.same_identity_as(&index));
1169 assert!(!data.same_identity_as(&schema));
1170 }
1171
1172 #[test]
1173 fn store_owns_explicit_stable_storage_config() {
1174 let store = Store::new_stable(
1175 Def::new("demo::rpg", "CharacterStore"),
1176 "CHARACTER_STORE",
1177 "characters",
1178 "demo::rpg::Canister",
1179 StoreStableMemoryConfig::new(110, 111, 112),
1180 );
1181
1182 assert!(store.is_stable_storage());
1183 assert!(store.storage().stable_memory_config().is_some());
1184 let stable = store
1185 .stable_memory_config()
1186 .expect("0.167 model stores stable config explicitly");
1187
1188 assert_eq!(stable.data_memory_id(), 110);
1189 assert_eq!(stable.index_memory_id(), 111);
1190 assert_eq!(stable.schema_memory_id(), 112);
1191 assert_eq!(store.stable_data_memory_id(), 110);
1192 assert_eq!(store.stable_index_memory_id(), 111);
1193 assert_eq!(store.stable_schema_memory_id(), 112);
1194 }
1195
1196 #[test]
1197 fn stable_store_storage_capabilities_describe_durable_contract() {
1198 let store = Store::new_stable(
1199 Def::new("demo::rpg", "CharacterStore"),
1200 "CHARACTER_STORE",
1201 "characters",
1202 "demo::rpg::Canister",
1203 StoreStableMemoryConfig::new(110, 111, 112),
1204 );
1205 let capabilities = store.storage_capabilities();
1206
1207 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Stable);
1208 assert_eq!(
1209 capabilities.allocation_identity(),
1210 AllocationIdentityCapability::Present,
1211 );
1212 assert_eq!(capabilities.durability(), StoreDurability::Durable);
1213 assert_eq!(
1214 capabilities.recovery(),
1215 StoreRecoveryCapability::StableCommitReplay,
1216 );
1217 assert_eq!(
1218 capabilities.commit_participation(),
1219 CommitParticipation::Durable,
1220 );
1221 assert_eq!(
1222 capabilities.schema_metadata(),
1223 SchemaMetadataCapability::DurableAcceptedHistory,
1224 );
1225 assert_eq!(
1226 capabilities.relation_source(),
1227 RelationSourceCapability::DurableSource,
1228 );
1229 assert_eq!(
1230 capabilities.relation_target(),
1231 RelationTargetCapability::DurableTarget,
1232 );
1233 assert_eq!(
1234 capabilities.live_validation(),
1235 LiveValidationCapability::Supported,
1236 );
1237 assert!(capabilities.has_allocation_identity());
1238 assert!(capabilities.participates_in_durable_commit());
1239 assert!(!capabilities.is_volatile());
1240 }
1241
1242 #[test]
1243 fn store_owns_explicit_heap_storage_config() {
1244 insert_canister("store_heap_config", "Canister");
1245 let store = Store::new_heap(
1246 Def::new("store_heap_config", "Store"),
1247 "STORE",
1248 "heap_store",
1249 "store_heap_config::Canister",
1250 StoreHeapConfig::new(),
1251 );
1252
1253 assert!(store.is_heap_storage());
1254 assert!(!store.is_stable_storage());
1255 assert!(store.stable_memory_config().is_none());
1256 assert!(store.validate().is_ok());
1257 }
1258
1259 #[test]
1260 fn heap_store_storage_capabilities_describe_volatile_contract() {
1261 let store = Store::new_heap(
1262 Def::new("store_heap_capabilities", "Store"),
1263 "STORE",
1264 "heap_store",
1265 "store_heap_capabilities::Canister",
1266 StoreHeapConfig::new(),
1267 );
1268 let capabilities = store.storage_capabilities();
1269
1270 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
1271 assert_eq!(
1272 capabilities.allocation_identity(),
1273 AllocationIdentityCapability::Absent,
1274 );
1275 assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1276 assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1277 assert_eq!(
1278 capabilities.commit_participation(),
1279 CommitParticipation::LiveOnly,
1280 );
1281 assert_eq!(
1282 capabilities.schema_metadata(),
1283 SchemaMetadataCapability::LiveRebuiltMetadata,
1284 );
1285 assert_eq!(
1286 capabilities.relation_source(),
1287 RelationSourceCapability::LiveSource,
1288 );
1289 assert_eq!(
1290 capabilities.relation_target(),
1291 RelationTargetCapability::VolatileTarget,
1292 );
1293 assert_eq!(
1294 capabilities.live_validation(),
1295 LiveValidationCapability::Supported,
1296 );
1297 assert!(!capabilities.has_allocation_identity());
1298 assert!(!capabilities.participates_in_durable_commit());
1299 assert!(capabilities.is_volatile());
1300 }
1301
1302 #[test]
1303 fn store_owns_explicit_journaled_storage_config() {
1304 insert_canister("store_journaled_config", "Canister");
1305 let store = Store::new_journaled(
1306 Def::new("store_journaled_config", "Store"),
1307 "STORE",
1308 "journaled_store",
1309 "store_journaled_config::Canister",
1310 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1311 );
1312
1313 assert!(store.is_journaled_storage());
1314 assert!(!store.is_stable_storage());
1315 assert!(!store.is_heap_storage());
1316 let journaled = store
1317 .journaled_memory_config()
1318 .expect("journaled model stores four-role config explicitly");
1319
1320 assert_eq!(journaled.data_memory_id(), 110);
1321 assert_eq!(journaled.index_memory_id(), 111);
1322 assert_eq!(journaled.schema_memory_id(), 112);
1323 assert_eq!(journaled.journal_memory_id(), 113);
1324 assert_eq!(store.stable_data_memory_id(), 110);
1325 assert_eq!(store.stable_index_memory_id(), 111);
1326 assert_eq!(store.stable_schema_memory_id(), 112);
1327 assert_eq!(store.journal_memory_id(), 113);
1328 assert!(store.validate().is_ok());
1329 }
1330
1331 #[test]
1332 fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
1333 let store = Store::new_journaled(
1334 Def::new("store_journaled_capabilities", "Store"),
1335 "STORE",
1336 "journaled_store",
1337 "store_journaled_capabilities::Canister",
1338 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1339 );
1340 let capabilities = store.storage_capabilities();
1341
1342 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1343 assert_eq!(
1344 capabilities.allocation_identity(),
1345 AllocationIdentityCapability::Present,
1346 );
1347 assert_eq!(capabilities.durability(), StoreDurability::Durable);
1348 assert_eq!(
1349 capabilities.recovery(),
1350 StoreRecoveryCapability::StableBasePlusJournalReplay,
1351 );
1352 assert_eq!(
1353 capabilities.commit_participation(),
1354 CommitParticipation::Durable,
1355 );
1356 assert_eq!(
1357 capabilities.schema_metadata(),
1358 SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1359 );
1360 assert_eq!(
1361 capabilities.relation_source(),
1362 RelationSourceCapability::DurableSource,
1363 );
1364 assert_eq!(
1365 capabilities.relation_target(),
1366 RelationTargetCapability::DurableTarget,
1367 );
1368 assert_eq!(
1369 capabilities.live_validation(),
1370 LiveValidationCapability::Supported,
1371 );
1372 assert!(capabilities.has_allocation_identity());
1373 assert!(capabilities.participates_in_durable_commit());
1374 assert!(!capabilities.is_volatile());
1375 }
1376
1377 #[test]
1378 fn journaled_store_allocations_use_role_named_stable_keys() {
1379 let store = Store::new_journaled(
1380 Def::new("demo::rpg", "CharacterStore"),
1381 "CHARACTER_STORE",
1382 "characters",
1383 "demo::rpg::Canister",
1384 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1385 );
1386
1387 assert_eq!(
1388 store.stable_data_allocation("demo_rpg").stable_key(),
1389 "icydb.demo_rpg.characters.data.v1",
1390 );
1391 assert_eq!(
1392 store.stable_index_allocation("demo_rpg").stable_key(),
1393 "icydb.demo_rpg.characters.index.v1",
1394 );
1395 assert_eq!(
1396 store.stable_schema_allocation("demo_rpg").stable_key(),
1397 "icydb.demo_rpg.characters.schema.v1",
1398 );
1399 assert_eq!(
1400 store.journal_allocation("demo_rpg").stable_key(),
1401 "icydb.demo_rpg.characters.journal.v1",
1402 );
1403 }
1404
1405 #[test]
1406 fn storage_capabilities_are_not_allocation_identity() {
1407 let store_a = Store::new_stable(
1408 Def::new("demo::rpg", "CharacterStore"),
1409 "CHARACTER_STORE",
1410 "characters",
1411 "demo::rpg::Canister",
1412 StoreStableMemoryConfig::new(110, 111, 112),
1413 );
1414 let store_b = Store::new_stable(
1415 Def::new("demo::rpg", "InventoryStore"),
1416 "INVENTORY_STORE",
1417 "inventory",
1418 "demo::rpg::Canister",
1419 StoreStableMemoryConfig::new(120, 121, 122),
1420 );
1421
1422 assert_eq!(
1423 store_a.storage_capabilities(),
1424 store_b.storage_capabilities()
1425 );
1426 assert_ne!(
1427 store_a.stable_data_allocation("demo_rpg"),
1428 store_b.stable_data_allocation("demo_rpg"),
1429 "stable allocation identity must remain separate from capabilities",
1430 );
1431 }
1432
1433 #[test]
1434 fn capability_consumers_use_axes_not_storage_mode() {
1435 const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1436 match capabilities.commit_participation() {
1437 CommitParticipation::Durable => "durable",
1438 CommitParticipation::LiveOnly => "live-only",
1439 }
1440 }
1441
1442 let future_durable_heap_mode = StoreStorageCapabilities {
1443 storage_mode: StoreStorageMode::Heap,
1444 allocation_identity: AllocationIdentityCapability::Present,
1445 durability: StoreDurability::Durable,
1446 recovery: StoreRecoveryCapability::StableCommitReplay,
1447 commit_participation: CommitParticipation::Durable,
1448 schema_metadata: SchemaMetadataCapability::DurableAcceptedHistory,
1449 relation_source: RelationSourceCapability::DurableSource,
1450 relation_target: RelationTargetCapability::DurableTarget,
1451 live_validation: LiveValidationCapability::Supported,
1452 };
1453
1454 assert_eq!(commit_label(future_durable_heap_mode), "durable");
1455 assert!(future_durable_heap_mode.participates_in_durable_commit());
1456 assert_eq!(
1457 future_durable_heap_mode.storage_mode(),
1458 StoreStorageMode::Heap,
1459 "the diagnostic storage mode must not drive commit policy",
1460 );
1461 }
1462
1463 #[test]
1464 fn store_stable_storage_config_rejects_duplicate_role_memory_ids() {
1465 insert_canister("store_duplicate_role_memory_ids", "Canister");
1466 let store = Store::new_stable(
1467 Def::new("store_duplicate_role_memory_ids", "Store"),
1468 "STORE",
1469 "duplicate_role_memory_ids",
1470 "store_duplicate_role_memory_ids::Canister",
1471 StoreStableMemoryConfig::new(110, 110, 112),
1472 );
1473
1474 let err = store
1475 .validate()
1476 .expect_err("duplicate store role memory IDs must fail validation");
1477 let rendered = err.to_string();
1478
1479 assert!(
1480 rendered.contains("data_memory_id and index_memory_id must differ"),
1481 "expected duplicate role memory-id error, got: {rendered}"
1482 );
1483 }
1484
1485 #[test]
1486 fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1487 insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1488 let store = Store::new_journaled(
1489 Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1490 "STORE",
1491 "duplicate_journaled_role_memory_ids",
1492 "store_duplicate_journaled_role_memory_ids::Canister",
1493 StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1494 );
1495
1496 let err = store
1497 .validate()
1498 .expect_err("duplicate journaled role memory IDs must fail validation");
1499 let rendered = err.to_string();
1500
1501 assert!(
1502 rendered.contains("schema_memory_id and journal_memory_id must differ"),
1503 "expected duplicate journaled role memory-id error, got: {rendered}"
1504 );
1505 }
1506}