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)]
30pub enum StoreStorage {
31 Stable(StoreStableMemoryConfig),
34}
35
36impl StoreStorage {
37 #[must_use]
42 pub const fn stable_memory_config(&self) -> &StoreStableMemoryConfig {
43 match self {
44 Self::Stable(config) => config,
45 }
46 }
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
51pub struct StoreStableMemoryConfig {
52 data: u8,
53 index: u8,
54 schema: u8,
55}
56
57impl StoreStableMemoryConfig {
58 #[must_use]
61 pub const fn new(data_memory_id: u8, index_memory_id: u8, schema_memory_id: u8) -> Self {
62 Self {
63 data: data_memory_id,
64 index: index_memory_id,
65 schema: schema_memory_id,
66 }
67 }
68
69 #[must_use]
71 pub const fn data_memory_id(self) -> u8 {
72 self.data
73 }
74
75 #[must_use]
77 pub const fn index_memory_id(self) -> u8 {
78 self.index
79 }
80
81 #[must_use]
83 pub const fn schema_memory_id(self) -> u8 {
84 self.schema
85 }
86}
87
88impl Store {
89 #[must_use]
93 pub const fn new_stable(
94 def: Def,
95 ident: &'static str,
96 store_name: &'static str,
97 canister: &'static str,
98 stable: StoreStableMemoryConfig,
99 ) -> Self {
100 Self {
101 def,
102 ident,
103 name: store_name,
104 canister,
105 storage: StoreStorage::Stable(stable),
106 }
107 }
108
109 #[must_use]
110 pub const fn def(&self) -> &Def {
111 &self.def
112 }
113
114 #[must_use]
115 pub const fn ident(&self) -> &'static str {
116 self.ident
117 }
118
119 #[must_use]
120 pub const fn store_name(&self) -> &'static str {
121 self.name
122 }
123
124 #[must_use]
125 pub const fn canister(&self) -> &'static str {
126 self.canister
127 }
128
129 #[must_use]
131 pub const fn storage(&self) -> &StoreStorage {
132 &self.storage
133 }
134
135 #[must_use]
137 pub const fn is_stable_storage(&self) -> bool {
138 matches!(self.storage, StoreStorage::Stable(_))
139 }
140
141 #[must_use]
143 pub const fn stable_memory_config(&self) -> Option<&StoreStableMemoryConfig> {
144 match &self.storage {
145 StoreStorage::Stable(config) => Some(config),
146 }
147 }
148
149 #[must_use]
150 pub const fn data_memory_id(&self) -> u8 {
151 self.storage.stable_memory_config().data_memory_id()
152 }
153
154 #[must_use]
155 pub const fn index_memory_id(&self) -> u8 {
156 self.storage.stable_memory_config().index_memory_id()
157 }
158
159 #[must_use]
160 pub const fn schema_memory_id(&self) -> u8 {
161 self.storage.stable_memory_config().schema_memory_id()
162 }
163
164 #[must_use]
165 pub fn data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
166 self.allocation(memory_namespace, StoreMemoryRole::Data)
167 }
168
169 #[must_use]
172 pub fn data_allocation_with_schema_metadata(
173 &self,
174 memory_namespace: &str,
175 schema_metadata: StableMemoryAllocationMetadata,
176 ) -> StableMemoryAllocation {
177 self.allocation_with_schema_metadata(
178 memory_namespace,
179 StoreMemoryRole::Data,
180 schema_metadata,
181 )
182 }
183
184 #[must_use]
185 pub fn index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
186 self.allocation(memory_namespace, StoreMemoryRole::Index)
187 }
188
189 #[must_use]
192 pub fn index_allocation_with_schema_metadata(
193 &self,
194 memory_namespace: &str,
195 schema_metadata: StableMemoryAllocationMetadata,
196 ) -> StableMemoryAllocation {
197 self.allocation_with_schema_metadata(
198 memory_namespace,
199 StoreMemoryRole::Index,
200 schema_metadata,
201 )
202 }
203
204 #[must_use]
205 pub fn schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
206 self.allocation(memory_namespace, StoreMemoryRole::Schema)
207 }
208
209 #[must_use]
212 pub fn schema_allocation_with_schema_metadata(
213 &self,
214 memory_namespace: &str,
215 schema_metadata: StableMemoryAllocationMetadata,
216 ) -> StableMemoryAllocation {
217 self.allocation_with_schema_metadata(
218 memory_namespace,
219 StoreMemoryRole::Schema,
220 schema_metadata,
221 )
222 }
223
224 #[must_use]
225 pub fn allocation(
226 &self,
227 memory_namespace: &str,
228 role: StoreMemoryRole,
229 ) -> StableMemoryAllocation {
230 let memory_id = match role {
231 StoreMemoryRole::Data => self.data_memory_id(),
232 StoreMemoryRole::Index => self.index_memory_id(),
233 StoreMemoryRole::Schema => self.schema_memory_id(),
234 };
235
236 StableMemoryAllocation::without_schema_metadata(
237 memory_id,
238 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
239 )
240 }
241
242 fn allocation_with_schema_metadata(
243 &self,
244 memory_namespace: &str,
245 role: StoreMemoryRole,
246 schema_metadata: StableMemoryAllocationMetadata,
247 ) -> StableMemoryAllocation {
248 let memory_id = match role {
249 StoreMemoryRole::Data => self.data_memory_id(),
250 StoreMemoryRole::Index => self.index_memory_id(),
251 StoreMemoryRole::Schema => self.schema_memory_id(),
252 };
253
254 StableMemoryAllocation::with_schema_metadata(
255 memory_id,
256 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
257 schema_metadata,
258 )
259 }
260}
261
262#[derive(Clone, Copy, Debug, Eq, PartialEq)]
263pub enum StoreMemoryRole {
264 Data,
265 Index,
266 Schema,
267}
268
269impl StoreMemoryRole {
270 #[must_use]
271 pub const fn as_str(self) -> &'static str {
272 match self {
273 Self::Data => "data",
274 Self::Index => "index",
275 Self::Schema => "schema",
276 }
277 }
278}
279
280#[derive(Clone, Debug, Eq, PartialEq)]
285pub struct StableMemoryAllocationMetadata {
286 schema_version: Option<u32>,
287 schema_fingerprint: Option<String>,
288}
289
290impl StableMemoryAllocationMetadata {
291 const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
292 Self {
293 schema_version,
294 schema_fingerprint,
295 }
296 }
297
298 #[must_use]
300 pub const fn from_accepted_schema_contract(
301 schema_version: u32,
302 schema_fingerprint: String,
303 ) -> Self {
304 Self::new(Some(schema_version), Some(schema_fingerprint))
305 }
306
307 #[must_use]
310 pub const fn absent() -> Self {
311 Self::new(None, None)
312 }
313
314 #[must_use]
316 pub const fn schema_version(&self) -> Option<u32> {
317 self.schema_version
318 }
319
320 #[must_use]
322 pub const fn schema_fingerprint(&self) -> Option<&str> {
323 match &self.schema_fingerprint {
324 Some(value) => Some(value.as_str()),
325 None => None,
326 }
327 }
328}
329
330#[derive(Clone, Debug, Eq, PartialEq)]
335pub struct StableMemoryAllocation {
336 memory_id: u8,
337 stable_key: String,
338 schema_metadata: StableMemoryAllocationMetadata,
339}
340
341impl StableMemoryAllocation {
342 #[must_use]
344 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
345 Self::with_schema_metadata(
346 memory_id,
347 stable_key,
348 StableMemoryAllocationMetadata::absent(),
349 )
350 }
351
352 #[must_use]
357 pub const fn with_schema_metadata(
358 memory_id: u8,
359 stable_key: String,
360 schema_metadata: StableMemoryAllocationMetadata,
361 ) -> Self {
362 Self {
363 memory_id,
364 stable_key,
365 schema_metadata,
366 }
367 }
368
369 #[must_use]
371 pub const fn memory_id(&self) -> u8 {
372 self.memory_id
373 }
374
375 #[must_use]
377 pub const fn stable_key(&self) -> &str {
378 self.stable_key.as_str()
379 }
380
381 #[must_use]
383 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
384 &self.schema_metadata
385 }
386
387 #[must_use]
389 pub const fn schema_version(&self) -> Option<u32> {
390 self.schema_metadata.schema_version()
391 }
392
393 #[must_use]
395 pub const fn schema_fingerprint(&self) -> Option<&str> {
396 self.schema_metadata.schema_fingerprint()
397 }
398
399 #[must_use]
404 pub fn same_identity_as(&self, other: &Self) -> bool {
405 self.memory_id == other.memory_id && self.stable_key == other.stable_key
406 }
407}
408
409#[must_use]
410pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
411 format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
412}
413
414impl MacroNode for Store {
415 fn as_any(&self) -> &dyn std::any::Any {
416 self
417 }
418}
419
420impl ValidateNode for Store {
421 fn validate(&self) -> Result<(), ErrorTree> {
422 let mut errs = ErrorTree::new();
423 let schema = schema_read();
424
425 match schema.cast_node::<Canister>(self.canister()) {
426 Ok(canister) => {
427 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
428 match self.storage() {
429 StoreStorage::Stable(config) => {
430 validate_stable_memory_config(&mut errs, self, *config, canister);
431 }
432 }
433 }
434 Err(e) => errs.add(e),
435 }
436
437 errs.result()
438 }
439}
440
441fn validate_stable_memory_config(
442 errs: &mut ErrorTree,
443 store: &Store,
444 config: StoreStableMemoryConfig,
445 canister: &Canister,
446) {
447 validate_stable_memory_role(
448 errs,
449 "data_memory_id",
450 "data stable key",
451 config.data_memory_id(),
452 store
453 .data_allocation(canister.memory_namespace())
454 .stable_key(),
455 canister,
456 );
457 validate_stable_memory_role(
458 errs,
459 "index_memory_id",
460 "index stable key",
461 config.index_memory_id(),
462 store
463 .index_allocation(canister.memory_namespace())
464 .stable_key(),
465 canister,
466 );
467 validate_stable_memory_role(
468 errs,
469 "schema_memory_id",
470 "schema stable key",
471 config.schema_memory_id(),
472 store
473 .schema_allocation(canister.memory_namespace())
474 .stable_key(),
475 canister,
476 );
477
478 if config.data_memory_id() == config.index_memory_id() {
479 err!(
480 errs,
481 "data_memory_id and index_memory_id must differ (both are {})",
482 config.data_memory_id(),
483 );
484 }
485 if config.data_memory_id() == config.schema_memory_id() {
486 err!(
487 errs,
488 "data_memory_id and schema_memory_id must differ (both are {})",
489 config.data_memory_id(),
490 );
491 }
492 if config.index_memory_id() == config.schema_memory_id() {
493 err!(
494 errs,
495 "index_memory_id and schema_memory_id must differ (both are {})",
496 config.index_memory_id(),
497 );
498 }
499}
500
501fn validate_stable_memory_role(
502 errs: &mut ErrorTree,
503 memory_label: &str,
504 stable_key_label: &str,
505 memory_id: u8,
506 stable_key: &str,
507 canister: &Canister,
508) {
509 validate_memory_id_in_range(
510 errs,
511 memory_label,
512 memory_id,
513 canister.memory_min(),
514 canister.memory_max(),
515 );
516 validate_app_memory_id(errs, memory_label, memory_id);
517 validate_memory_id_not_reserved(errs, memory_label, memory_id);
518 validate_stable_key(errs, stable_key_label, stable_key);
519}
520
521impl VisitableNode for Store {
522 fn route_key(&self) -> String {
523 self.def().path()
524 }
525
526 fn drive<V: Visitor>(&self, v: &mut V) {
527 self.def().accept(v);
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use crate::{
534 build::schema_write,
535 node::{Canister, SchemaNode},
536 };
537
538 use super::*;
539
540 fn insert_canister(path_module: &'static str, ident: &'static str) {
541 schema_write().insert_node(SchemaNode::Canister(Canister::new(
542 Def::new(path_module, ident),
543 "test_db",
544 100,
545 254,
546 254,
547 )));
548 }
549
550 #[test]
551 fn store_stable_keys_use_durable_icydb_shape() {
552 let store = Store::new_stable(
553 Def::new("demo::rpg", "CharacterStore"),
554 "CHARACTER_STORE",
555 "characters",
556 "demo::rpg::Canister",
557 StoreStableMemoryConfig::new(110, 111, 112),
558 );
559
560 assert_eq!(
561 store.data_allocation("demo_rpg").stable_key(),
562 "icydb.demo_rpg.characters.data.v1",
563 );
564 assert_eq!(
565 store.index_allocation("demo_rpg").stable_key(),
566 "icydb.demo_rpg.characters.index.v1",
567 );
568 assert_eq!(
569 store.schema_allocation("demo_rpg").stable_key(),
570 "icydb.demo_rpg.characters.schema.v1",
571 );
572 }
573
574 #[test]
575 fn store_allocations_default_to_absent_schema_metadata() {
576 let store = Store::new_stable(
577 Def::new("demo::rpg", "CharacterStore"),
578 "CHARACTER_STORE",
579 "characters",
580 "demo::rpg::Canister",
581 StoreStableMemoryConfig::new(110, 111, 112),
582 );
583
584 for allocation in [
585 store.data_allocation("demo_rpg"),
586 store.index_allocation("demo_rpg"),
587 store.schema_allocation("demo_rpg"),
588 ] {
589 assert_eq!(allocation.schema_version(), None);
590 assert_eq!(allocation.schema_fingerprint(), None);
591 assert_eq!(
592 allocation.schema_metadata(),
593 &StableMemoryAllocationMetadata::absent()
594 );
595 }
596 }
597
598 #[test]
599 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
600 let store = Store::new_stable(
601 Def::new("demo::rpg", "CharacterStore"),
602 "CHARACTER_STORE",
603 "characters",
604 "demo::rpg::Canister",
605 StoreStableMemoryConfig::new(110, 111, 112),
606 );
607 let data = store.data_allocation_with_schema_metadata(
608 "demo_rpg",
609 StableMemoryAllocationMetadata::from_accepted_schema_contract(
610 7,
611 "data-row-layout".to_string(),
612 ),
613 );
614 let index = store.index_allocation_with_schema_metadata(
615 "demo_rpg",
616 StableMemoryAllocationMetadata::from_accepted_schema_contract(
617 8,
618 "index-catalog".to_string(),
619 ),
620 );
621 let schema = store.schema_allocation_with_schema_metadata(
622 "demo_rpg",
623 StableMemoryAllocationMetadata::from_accepted_schema_contract(
624 10,
625 "schema-catalog".to_string(),
626 ),
627 );
628 let data_after_reconcile = store.data_allocation_with_schema_metadata(
629 "demo_rpg",
630 StableMemoryAllocationMetadata::from_accepted_schema_contract(
631 9,
632 "data-row-layout-v2".to_string(),
633 ),
634 );
635
636 assert_eq!(data.schema_version(), Some(7));
637 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
638 assert_eq!(index.schema_version(), Some(8));
639 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
640 assert_eq!(schema.schema_version(), Some(10));
641 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
642 assert!(data.same_identity_as(&data_after_reconcile));
643 assert!(!data.same_identity_as(&index));
644 assert!(!data.same_identity_as(&schema));
645 }
646
647 #[test]
648 fn store_owns_explicit_stable_storage_config() {
649 let store = Store::new_stable(
650 Def::new("demo::rpg", "CharacterStore"),
651 "CHARACTER_STORE",
652 "characters",
653 "demo::rpg::Canister",
654 StoreStableMemoryConfig::new(110, 111, 112),
655 );
656
657 assert!(store.is_stable_storage());
658 let stable = store
659 .stable_memory_config()
660 .expect("0.167 model stores stable config explicitly");
661
662 assert_eq!(stable.data_memory_id(), 110);
663 assert_eq!(stable.index_memory_id(), 111);
664 assert_eq!(stable.schema_memory_id(), 112);
665 assert_eq!(store.data_memory_id(), 110);
666 assert_eq!(store.index_memory_id(), 111);
667 assert_eq!(store.schema_memory_id(), 112);
668 }
669
670 #[test]
671 fn store_stable_storage_config_rejects_duplicate_role_memory_ids() {
672 insert_canister("store_duplicate_role_memory_ids", "Canister");
673 let store = Store::new_stable(
674 Def::new("store_duplicate_role_memory_ids", "Store"),
675 "STORE",
676 "duplicate_role_memory_ids",
677 "store_duplicate_role_memory_ids::Canister",
678 StoreStableMemoryConfig::new(110, 110, 112),
679 );
680
681 let err = store
682 .validate()
683 .expect_err("duplicate store role memory IDs must fail validation");
684 let rendered = err.to_string();
685
686 assert!(
687 rendered.contains("data_memory_id and index_memory_id must differ"),
688 "expected duplicate role memory-id error, got: {rendered}"
689 );
690 }
691}