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 schema_version: Option<u32>,
700 schema_fingerprint: Option<String>,
701}
702
703impl StableMemoryAllocationMetadata {
704 const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
705 Self {
706 schema_version,
707 schema_fingerprint,
708 }
709 }
710
711 #[must_use]
713 pub const fn from_accepted_schema_contract(
714 schema_version: u32,
715 schema_fingerprint: String,
716 ) -> Self {
717 Self::new(Some(schema_version), Some(schema_fingerprint))
718 }
719
720 #[must_use]
723 pub const fn absent() -> Self {
724 Self::new(None, None)
725 }
726
727 #[must_use]
729 pub const fn schema_version(&self) -> Option<u32> {
730 self.schema_version
731 }
732
733 #[must_use]
735 pub const fn schema_fingerprint(&self) -> Option<&str> {
736 match &self.schema_fingerprint {
737 Some(value) => Some(value.as_str()),
738 None => None,
739 }
740 }
741}
742
743#[derive(Clone, Debug, Eq, PartialEq)]
748pub struct StableMemoryAllocation {
749 memory_id: u8,
750 stable_key: String,
751 schema_metadata: StableMemoryAllocationMetadata,
752}
753
754impl StableMemoryAllocation {
755 #[must_use]
757 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
758 Self::with_schema_metadata(
759 memory_id,
760 stable_key,
761 StableMemoryAllocationMetadata::absent(),
762 )
763 }
764
765 #[must_use]
770 pub const fn with_schema_metadata(
771 memory_id: u8,
772 stable_key: String,
773 schema_metadata: StableMemoryAllocationMetadata,
774 ) -> Self {
775 Self {
776 memory_id,
777 stable_key,
778 schema_metadata,
779 }
780 }
781
782 #[must_use]
784 pub const fn memory_id(&self) -> u8 {
785 self.memory_id
786 }
787
788 #[must_use]
790 pub const fn stable_key(&self) -> &str {
791 self.stable_key.as_str()
792 }
793
794 #[must_use]
796 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
797 &self.schema_metadata
798 }
799
800 #[must_use]
802 pub const fn schema_version(&self) -> Option<u32> {
803 self.schema_metadata.schema_version()
804 }
805
806 #[must_use]
808 pub const fn schema_fingerprint(&self) -> Option<&str> {
809 self.schema_metadata.schema_fingerprint()
810 }
811
812 #[must_use]
817 pub fn same_identity_as(&self, other: &Self) -> bool {
818 self.memory_id == other.memory_id && self.stable_key == other.stable_key
819 }
820}
821
822#[must_use]
823pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
824 format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
825}
826
827impl MacroNode for Store {
828 fn as_any(&self) -> &dyn std::any::Any {
829 self
830 }
831}
832
833impl ValidateNode for Store {
834 fn validate(&self) -> Result<(), ErrorTree> {
835 let mut errs = ErrorTree::new();
836 let schema = schema_read();
837
838 match schema.cast_node::<Canister>(self.canister()) {
839 Ok(canister) => {
840 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
841 match self.storage() {
842 StoreStorage::Stable(config) => {
843 validate_stable_memory_config(&mut errs, self, *config, canister);
844 }
845 StoreStorage::Heap(_) => {}
846 StoreStorage::Journaled(config) => {
847 validate_journaled_memory_config(&mut errs, self, *config, canister);
848 }
849 }
850 }
851 Err(e) => errs.add(e),
852 }
853
854 errs.result()
855 }
856}
857
858fn validate_journaled_memory_config(
859 errs: &mut ErrorTree,
860 store: &Store,
861 config: StoreJournaledMemoryConfig,
862 canister: &Canister,
863) {
864 validate_stable_memory_role(
865 errs,
866 "data_memory_id",
867 "data stable key",
868 config.data_memory_id(),
869 store
870 .stable_data_allocation(canister.memory_namespace())
871 .stable_key(),
872 canister,
873 );
874 validate_stable_memory_role(
875 errs,
876 "index_memory_id",
877 "index stable key",
878 config.index_memory_id(),
879 store
880 .stable_index_allocation(canister.memory_namespace())
881 .stable_key(),
882 canister,
883 );
884 validate_stable_memory_role(
885 errs,
886 "schema_memory_id",
887 "schema stable key",
888 config.schema_memory_id(),
889 store
890 .stable_schema_allocation(canister.memory_namespace())
891 .stable_key(),
892 canister,
893 );
894 validate_stable_memory_role(
895 errs,
896 "journal_memory_id",
897 "journal stable key",
898 config.journal_memory_id(),
899 store
900 .journal_allocation(canister.memory_namespace())
901 .stable_key(),
902 canister,
903 );
904
905 validate_distinct_journaled_memory_ids(errs, config);
906}
907
908fn validate_distinct_journaled_memory_ids(
909 errs: &mut ErrorTree,
910 config: StoreJournaledMemoryConfig,
911) {
912 let roles = [
913 ("data_memory_id", config.data_memory_id()),
914 ("index_memory_id", config.index_memory_id()),
915 ("schema_memory_id", config.schema_memory_id()),
916 ("journal_memory_id", config.journal_memory_id()),
917 ];
918
919 for (idx, (left_label, left_id)) in roles.iter().enumerate() {
920 for (right_label, right_id) in roles.iter().skip(idx + 1) {
921 if left_id == right_id {
922 err!(
923 errs,
924 "{} and {} must differ (both are {})",
925 left_label,
926 right_label,
927 left_id,
928 );
929 }
930 }
931 }
932}
933
934fn validate_stable_memory_config(
935 errs: &mut ErrorTree,
936 store: &Store,
937 config: StoreStableMemoryConfig,
938 canister: &Canister,
939) {
940 validate_stable_memory_role(
941 errs,
942 "data_memory_id",
943 "data stable key",
944 config.data_memory_id(),
945 store
946 .stable_data_allocation(canister.memory_namespace())
947 .stable_key(),
948 canister,
949 );
950 validate_stable_memory_role(
951 errs,
952 "index_memory_id",
953 "index stable key",
954 config.index_memory_id(),
955 store
956 .stable_index_allocation(canister.memory_namespace())
957 .stable_key(),
958 canister,
959 );
960 validate_stable_memory_role(
961 errs,
962 "schema_memory_id",
963 "schema stable key",
964 config.schema_memory_id(),
965 store
966 .stable_schema_allocation(canister.memory_namespace())
967 .stable_key(),
968 canister,
969 );
970
971 if config.data_memory_id() == config.index_memory_id() {
972 err!(
973 errs,
974 "data_memory_id and index_memory_id must differ (both are {})",
975 config.data_memory_id(),
976 );
977 }
978 if config.data_memory_id() == config.schema_memory_id() {
979 err!(
980 errs,
981 "data_memory_id and schema_memory_id must differ (both are {})",
982 config.data_memory_id(),
983 );
984 }
985 if config.index_memory_id() == config.schema_memory_id() {
986 err!(
987 errs,
988 "index_memory_id and schema_memory_id must differ (both are {})",
989 config.index_memory_id(),
990 );
991 }
992}
993
994fn validate_stable_memory_role(
995 errs: &mut ErrorTree,
996 memory_label: &str,
997 stable_key_label: &str,
998 memory_id: u8,
999 stable_key: &str,
1000 canister: &Canister,
1001) {
1002 validate_memory_id_in_range(
1003 errs,
1004 memory_label,
1005 memory_id,
1006 canister.memory_min(),
1007 canister.memory_max(),
1008 );
1009 validate_app_memory_id(errs, memory_label, memory_id);
1010 validate_memory_id_not_reserved(errs, memory_label, memory_id);
1011 validate_stable_key(errs, stable_key_label, stable_key);
1012}
1013
1014impl VisitableNode for Store {
1015 fn route_key(&self) -> String {
1016 self.def().path()
1017 }
1018
1019 fn drive<V: Visitor>(&self, v: &mut V) {
1020 self.def().accept(v);
1021 }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026 use crate::{
1027 build::schema_write,
1028 node::{Canister, SchemaNode},
1029 };
1030
1031 use super::*;
1032
1033 fn insert_canister(path_module: &'static str, ident: &'static str) {
1034 schema_write().insert_node(SchemaNode::Canister(Canister::new(
1035 Def::new(path_module, ident),
1036 "test_db",
1037 100,
1038 254,
1039 254,
1040 )));
1041 }
1042
1043 #[test]
1044 fn store_stable_keys_use_durable_icydb_shape() {
1045 let store = Store::new_stable(
1046 Def::new("demo::rpg", "CharacterStore"),
1047 "CHARACTER_STORE",
1048 "characters",
1049 "demo::rpg::Canister",
1050 StoreStableMemoryConfig::new(110, 111, 112),
1051 );
1052
1053 assert_eq!(
1054 store.stable_data_allocation("demo_rpg").stable_key(),
1055 "icydb.demo_rpg.characters.data.v1",
1056 );
1057 assert_eq!(
1058 store.stable_index_allocation("demo_rpg").stable_key(),
1059 "icydb.demo_rpg.characters.index.v1",
1060 );
1061 assert_eq!(
1062 store.stable_schema_allocation("demo_rpg").stable_key(),
1063 "icydb.demo_rpg.characters.schema.v1",
1064 );
1065 }
1066
1067 #[test]
1068 fn store_allocations_default_to_absent_schema_metadata() {
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 for allocation in [
1078 store.stable_data_allocation("demo_rpg"),
1079 store.stable_index_allocation("demo_rpg"),
1080 store.stable_schema_allocation("demo_rpg"),
1081 ] {
1082 assert_eq!(allocation.schema_version(), None);
1083 assert_eq!(allocation.schema_fingerprint(), None);
1084 assert_eq!(
1085 allocation.schema_metadata(),
1086 &StableMemoryAllocationMetadata::absent()
1087 );
1088 }
1089 }
1090
1091 #[test]
1092 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
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 let data = store.stable_data_allocation_with_schema_metadata(
1101 "demo_rpg",
1102 StableMemoryAllocationMetadata::from_accepted_schema_contract(
1103 7,
1104 "data-row-layout".to_string(),
1105 ),
1106 );
1107 let index = store.stable_index_allocation_with_schema_metadata(
1108 "demo_rpg",
1109 StableMemoryAllocationMetadata::from_accepted_schema_contract(
1110 8,
1111 "index-catalog".to_string(),
1112 ),
1113 );
1114 let schema = store.stable_schema_allocation_with_schema_metadata(
1115 "demo_rpg",
1116 StableMemoryAllocationMetadata::from_accepted_schema_contract(
1117 10,
1118 "schema-catalog".to_string(),
1119 ),
1120 );
1121 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
1122 "demo_rpg",
1123 StableMemoryAllocationMetadata::from_accepted_schema_contract(
1124 9,
1125 "data-row-layout-v2".to_string(),
1126 ),
1127 );
1128
1129 assert_eq!(data.schema_version(), Some(7));
1130 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
1131 assert_eq!(index.schema_version(), Some(8));
1132 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
1133 assert_eq!(schema.schema_version(), Some(10));
1134 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
1135 assert!(data.same_identity_as(&data_after_reconcile));
1136 assert!(!data.same_identity_as(&index));
1137 assert!(!data.same_identity_as(&schema));
1138 }
1139
1140 #[test]
1141 fn store_owns_explicit_stable_storage_config() {
1142 let store = Store::new_stable(
1143 Def::new("demo::rpg", "CharacterStore"),
1144 "CHARACTER_STORE",
1145 "characters",
1146 "demo::rpg::Canister",
1147 StoreStableMemoryConfig::new(110, 111, 112),
1148 );
1149
1150 assert!(store.is_stable_storage());
1151 assert!(store.storage().stable_memory_config().is_some());
1152 let stable = store
1153 .stable_memory_config()
1154 .expect("0.167 model stores stable config explicitly");
1155
1156 assert_eq!(stable.data_memory_id(), 110);
1157 assert_eq!(stable.index_memory_id(), 111);
1158 assert_eq!(stable.schema_memory_id(), 112);
1159 assert_eq!(store.stable_data_memory_id(), 110);
1160 assert_eq!(store.stable_index_memory_id(), 111);
1161 assert_eq!(store.stable_schema_memory_id(), 112);
1162 }
1163
1164 #[test]
1165 fn stable_store_storage_capabilities_describe_durable_contract() {
1166 let store = Store::new_stable(
1167 Def::new("demo::rpg", "CharacterStore"),
1168 "CHARACTER_STORE",
1169 "characters",
1170 "demo::rpg::Canister",
1171 StoreStableMemoryConfig::new(110, 111, 112),
1172 );
1173 let capabilities = store.storage_capabilities();
1174
1175 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Stable);
1176 assert_eq!(
1177 capabilities.allocation_identity(),
1178 AllocationIdentityCapability::Present,
1179 );
1180 assert_eq!(capabilities.durability(), StoreDurability::Durable);
1181 assert_eq!(
1182 capabilities.recovery(),
1183 StoreRecoveryCapability::StableCommitReplay,
1184 );
1185 assert_eq!(
1186 capabilities.commit_participation(),
1187 CommitParticipation::Durable,
1188 );
1189 assert_eq!(
1190 capabilities.schema_metadata(),
1191 SchemaMetadataCapability::DurableAcceptedHistory,
1192 );
1193 assert_eq!(
1194 capabilities.relation_source(),
1195 RelationSourceCapability::DurableSource,
1196 );
1197 assert_eq!(
1198 capabilities.relation_target(),
1199 RelationTargetCapability::DurableTarget,
1200 );
1201 assert_eq!(
1202 capabilities.live_validation(),
1203 LiveValidationCapability::Supported,
1204 );
1205 assert!(capabilities.has_allocation_identity());
1206 assert!(capabilities.participates_in_durable_commit());
1207 assert!(!capabilities.is_volatile());
1208 }
1209
1210 #[test]
1211 fn store_owns_explicit_heap_storage_config() {
1212 insert_canister("store_heap_config", "Canister");
1213 let store = Store::new_heap(
1214 Def::new("store_heap_config", "Store"),
1215 "STORE",
1216 "heap_store",
1217 "store_heap_config::Canister",
1218 StoreHeapConfig::new(),
1219 );
1220
1221 assert!(store.is_heap_storage());
1222 assert!(!store.is_stable_storage());
1223 assert!(store.stable_memory_config().is_none());
1224 assert!(store.validate().is_ok());
1225 }
1226
1227 #[test]
1228 fn heap_store_storage_capabilities_describe_volatile_contract() {
1229 let store = Store::new_heap(
1230 Def::new("store_heap_capabilities", "Store"),
1231 "STORE",
1232 "heap_store",
1233 "store_heap_capabilities::Canister",
1234 StoreHeapConfig::new(),
1235 );
1236 let capabilities = store.storage_capabilities();
1237
1238 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Heap);
1239 assert_eq!(
1240 capabilities.allocation_identity(),
1241 AllocationIdentityCapability::Absent,
1242 );
1243 assert_eq!(capabilities.durability(), StoreDurability::Volatile);
1244 assert_eq!(capabilities.recovery(), StoreRecoveryCapability::None);
1245 assert_eq!(
1246 capabilities.commit_participation(),
1247 CommitParticipation::LiveOnly,
1248 );
1249 assert_eq!(
1250 capabilities.schema_metadata(),
1251 SchemaMetadataCapability::LiveRebuiltMetadata,
1252 );
1253 assert_eq!(
1254 capabilities.relation_source(),
1255 RelationSourceCapability::LiveSource,
1256 );
1257 assert_eq!(
1258 capabilities.relation_target(),
1259 RelationTargetCapability::VolatileTarget,
1260 );
1261 assert_eq!(
1262 capabilities.live_validation(),
1263 LiveValidationCapability::Supported,
1264 );
1265 assert!(!capabilities.has_allocation_identity());
1266 assert!(!capabilities.participates_in_durable_commit());
1267 assert!(capabilities.is_volatile());
1268 }
1269
1270 #[test]
1271 fn store_owns_explicit_journaled_storage_config() {
1272 insert_canister("store_journaled_config", "Canister");
1273 let store = Store::new_journaled(
1274 Def::new("store_journaled_config", "Store"),
1275 "STORE",
1276 "journaled_store",
1277 "store_journaled_config::Canister",
1278 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1279 );
1280
1281 assert!(store.is_journaled_storage());
1282 assert!(!store.is_stable_storage());
1283 assert!(!store.is_heap_storage());
1284 let journaled = store
1285 .journaled_memory_config()
1286 .expect("journaled model stores four-role config explicitly");
1287
1288 assert_eq!(journaled.data_memory_id(), 110);
1289 assert_eq!(journaled.index_memory_id(), 111);
1290 assert_eq!(journaled.schema_memory_id(), 112);
1291 assert_eq!(journaled.journal_memory_id(), 113);
1292 assert_eq!(store.stable_data_memory_id(), 110);
1293 assert_eq!(store.stable_index_memory_id(), 111);
1294 assert_eq!(store.stable_schema_memory_id(), 112);
1295 assert_eq!(store.journal_memory_id(), 113);
1296 assert!(store.validate().is_ok());
1297 }
1298
1299 #[test]
1300 fn journaled_store_storage_capabilities_describe_cached_stable_contract() {
1301 let store = Store::new_journaled(
1302 Def::new("store_journaled_capabilities", "Store"),
1303 "STORE",
1304 "journaled_store",
1305 "store_journaled_capabilities::Canister",
1306 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1307 );
1308 let capabilities = store.storage_capabilities();
1309
1310 assert_eq!(capabilities.storage_mode(), StoreStorageMode::Journaled);
1311 assert_eq!(
1312 capabilities.allocation_identity(),
1313 AllocationIdentityCapability::Present,
1314 );
1315 assert_eq!(capabilities.durability(), StoreDurability::Durable);
1316 assert_eq!(
1317 capabilities.recovery(),
1318 StoreRecoveryCapability::StableBasePlusJournalReplay,
1319 );
1320 assert_eq!(
1321 capabilities.commit_participation(),
1322 CommitParticipation::Durable,
1323 );
1324 assert_eq!(
1325 capabilities.schema_metadata(),
1326 SchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
1327 );
1328 assert_eq!(
1329 capabilities.relation_source(),
1330 RelationSourceCapability::DurableSource,
1331 );
1332 assert_eq!(
1333 capabilities.relation_target(),
1334 RelationTargetCapability::DurableTarget,
1335 );
1336 assert_eq!(
1337 capabilities.live_validation(),
1338 LiveValidationCapability::Supported,
1339 );
1340 assert!(capabilities.has_allocation_identity());
1341 assert!(capabilities.participates_in_durable_commit());
1342 assert!(!capabilities.is_volatile());
1343 }
1344
1345 #[test]
1346 fn journaled_store_allocations_use_role_named_stable_keys() {
1347 let store = Store::new_journaled(
1348 Def::new("demo::rpg", "CharacterStore"),
1349 "CHARACTER_STORE",
1350 "characters",
1351 "demo::rpg::Canister",
1352 StoreJournaledMemoryConfig::new(110, 111, 112, 113),
1353 );
1354
1355 assert_eq!(
1356 store.stable_data_allocation("demo_rpg").stable_key(),
1357 "icydb.demo_rpg.characters.data.v1",
1358 );
1359 assert_eq!(
1360 store.stable_index_allocation("demo_rpg").stable_key(),
1361 "icydb.demo_rpg.characters.index.v1",
1362 );
1363 assert_eq!(
1364 store.stable_schema_allocation("demo_rpg").stable_key(),
1365 "icydb.demo_rpg.characters.schema.v1",
1366 );
1367 assert_eq!(
1368 store.journal_allocation("demo_rpg").stable_key(),
1369 "icydb.demo_rpg.characters.journal.v1",
1370 );
1371 }
1372
1373 #[test]
1374 fn storage_capabilities_are_not_allocation_identity() {
1375 let store_a = Store::new_stable(
1376 Def::new("demo::rpg", "CharacterStore"),
1377 "CHARACTER_STORE",
1378 "characters",
1379 "demo::rpg::Canister",
1380 StoreStableMemoryConfig::new(110, 111, 112),
1381 );
1382 let store_b = Store::new_stable(
1383 Def::new("demo::rpg", "InventoryStore"),
1384 "INVENTORY_STORE",
1385 "inventory",
1386 "demo::rpg::Canister",
1387 StoreStableMemoryConfig::new(120, 121, 122),
1388 );
1389
1390 assert_eq!(
1391 store_a.storage_capabilities(),
1392 store_b.storage_capabilities()
1393 );
1394 assert_ne!(
1395 store_a.stable_data_allocation("demo_rpg"),
1396 store_b.stable_data_allocation("demo_rpg"),
1397 "stable allocation identity must remain separate from capabilities",
1398 );
1399 }
1400
1401 #[test]
1402 fn capability_consumers_use_axes_not_storage_mode() {
1403 const fn commit_label(capabilities: StoreStorageCapabilities) -> &'static str {
1404 match capabilities.commit_participation() {
1405 CommitParticipation::Durable => "durable",
1406 CommitParticipation::LiveOnly => "live-only",
1407 }
1408 }
1409
1410 let future_durable_heap_mode = StoreStorageCapabilities {
1411 storage_mode: StoreStorageMode::Heap,
1412 allocation_identity: AllocationIdentityCapability::Present,
1413 durability: StoreDurability::Durable,
1414 recovery: StoreRecoveryCapability::StableCommitReplay,
1415 commit_participation: CommitParticipation::Durable,
1416 schema_metadata: SchemaMetadataCapability::DurableAcceptedHistory,
1417 relation_source: RelationSourceCapability::DurableSource,
1418 relation_target: RelationTargetCapability::DurableTarget,
1419 live_validation: LiveValidationCapability::Supported,
1420 };
1421
1422 assert_eq!(commit_label(future_durable_heap_mode), "durable");
1423 assert!(future_durable_heap_mode.participates_in_durable_commit());
1424 assert_eq!(
1425 future_durable_heap_mode.storage_mode(),
1426 StoreStorageMode::Heap,
1427 "the diagnostic storage mode must not drive commit policy",
1428 );
1429 }
1430
1431 #[test]
1432 fn store_stable_storage_config_rejects_duplicate_role_memory_ids() {
1433 insert_canister("store_duplicate_role_memory_ids", "Canister");
1434 let store = Store::new_stable(
1435 Def::new("store_duplicate_role_memory_ids", "Store"),
1436 "STORE",
1437 "duplicate_role_memory_ids",
1438 "store_duplicate_role_memory_ids::Canister",
1439 StoreStableMemoryConfig::new(110, 110, 112),
1440 );
1441
1442 let err = store
1443 .validate()
1444 .expect_err("duplicate store role memory IDs must fail validation");
1445 let rendered = err.to_string();
1446
1447 assert!(
1448 rendered.contains("data_memory_id and index_memory_id must differ"),
1449 "expected duplicate role memory-id error, got: {rendered}"
1450 );
1451 }
1452
1453 #[test]
1454 fn store_journaled_storage_config_rejects_duplicate_role_memory_ids() {
1455 insert_canister("store_duplicate_journaled_role_memory_ids", "Canister");
1456 let store = Store::new_journaled(
1457 Def::new("store_duplicate_journaled_role_memory_ids", "Store"),
1458 "STORE",
1459 "duplicate_journaled_role_memory_ids",
1460 "store_duplicate_journaled_role_memory_ids::Canister",
1461 StoreJournaledMemoryConfig::new(110, 111, 112, 112),
1462 );
1463
1464 let err = store
1465 .validate()
1466 .expect_err("duplicate journaled role memory IDs must fail validation");
1467 let rendered = err.to_string();
1468
1469 assert!(
1470 rendered.contains("schema_memory_id and journal_memory_id must differ"),
1471 "expected duplicate journaled role memory-id error, got: {rendered}"
1472 );
1473 }
1474}