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) -> Option<&StoreStableMemoryConfig> {
43 match self {
44 Self::Stable(config) => Some(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 self.storage.stable_memory_config()
145 }
146
147 #[must_use]
148 pub const fn stable_data_memory_id(&self) -> u8 {
149 match self.storage {
150 StoreStorage::Stable(config) => config.data_memory_id(),
151 }
152 }
153
154 #[must_use]
155 pub const fn stable_index_memory_id(&self) -> u8 {
156 match self.storage {
157 StoreStorage::Stable(config) => config.index_memory_id(),
158 }
159 }
160
161 #[must_use]
162 pub const fn stable_schema_memory_id(&self) -> u8 {
163 match self.storage {
164 StoreStorage::Stable(config) => config.schema_memory_id(),
165 }
166 }
167
168 #[must_use]
169 pub fn stable_data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
170 self.stable_allocation(memory_namespace, StoreMemoryRole::Data)
171 }
172
173 #[must_use]
176 pub fn stable_data_allocation_with_schema_metadata(
177 &self,
178 memory_namespace: &str,
179 schema_metadata: StableMemoryAllocationMetadata,
180 ) -> StableMemoryAllocation {
181 self.stable_allocation_with_schema_metadata(
182 memory_namespace,
183 StoreMemoryRole::Data,
184 schema_metadata,
185 )
186 }
187
188 #[must_use]
189 pub fn stable_index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
190 self.stable_allocation(memory_namespace, StoreMemoryRole::Index)
191 }
192
193 #[must_use]
196 pub fn stable_index_allocation_with_schema_metadata(
197 &self,
198 memory_namespace: &str,
199 schema_metadata: StableMemoryAllocationMetadata,
200 ) -> StableMemoryAllocation {
201 self.stable_allocation_with_schema_metadata(
202 memory_namespace,
203 StoreMemoryRole::Index,
204 schema_metadata,
205 )
206 }
207
208 #[must_use]
209 pub fn stable_schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
210 self.stable_allocation(memory_namespace, StoreMemoryRole::Schema)
211 }
212
213 #[must_use]
216 pub fn stable_schema_allocation_with_schema_metadata(
217 &self,
218 memory_namespace: &str,
219 schema_metadata: StableMemoryAllocationMetadata,
220 ) -> StableMemoryAllocation {
221 self.stable_allocation_with_schema_metadata(
222 memory_namespace,
223 StoreMemoryRole::Schema,
224 schema_metadata,
225 )
226 }
227
228 #[must_use]
229 pub fn stable_allocation(
230 &self,
231 memory_namespace: &str,
232 role: StoreMemoryRole,
233 ) -> StableMemoryAllocation {
234 let memory_id = match role {
235 StoreMemoryRole::Data => self.stable_data_memory_id(),
236 StoreMemoryRole::Index => self.stable_index_memory_id(),
237 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
238 };
239
240 StableMemoryAllocation::without_schema_metadata(
241 memory_id,
242 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
243 )
244 }
245
246 fn stable_allocation_with_schema_metadata(
247 &self,
248 memory_namespace: &str,
249 role: StoreMemoryRole,
250 schema_metadata: StableMemoryAllocationMetadata,
251 ) -> StableMemoryAllocation {
252 let memory_id = match role {
253 StoreMemoryRole::Data => self.stable_data_memory_id(),
254 StoreMemoryRole::Index => self.stable_index_memory_id(),
255 StoreMemoryRole::Schema => self.stable_schema_memory_id(),
256 };
257
258 StableMemoryAllocation::with_schema_metadata(
259 memory_id,
260 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
261 schema_metadata,
262 )
263 }
264}
265
266#[derive(Clone, Copy, Debug, Eq, PartialEq)]
267pub enum StoreMemoryRole {
268 Data,
269 Index,
270 Schema,
271}
272
273impl StoreMemoryRole {
274 #[must_use]
275 pub const fn as_str(self) -> &'static str {
276 match self {
277 Self::Data => "data",
278 Self::Index => "index",
279 Self::Schema => "schema",
280 }
281 }
282}
283
284#[derive(Clone, Debug, Eq, PartialEq)]
289pub struct StableMemoryAllocationMetadata {
290 schema_version: Option<u32>,
291 schema_fingerprint: Option<String>,
292}
293
294impl StableMemoryAllocationMetadata {
295 const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
296 Self {
297 schema_version,
298 schema_fingerprint,
299 }
300 }
301
302 #[must_use]
304 pub const fn from_accepted_schema_contract(
305 schema_version: u32,
306 schema_fingerprint: String,
307 ) -> Self {
308 Self::new(Some(schema_version), Some(schema_fingerprint))
309 }
310
311 #[must_use]
314 pub const fn absent() -> Self {
315 Self::new(None, None)
316 }
317
318 #[must_use]
320 pub const fn schema_version(&self) -> Option<u32> {
321 self.schema_version
322 }
323
324 #[must_use]
326 pub const fn schema_fingerprint(&self) -> Option<&str> {
327 match &self.schema_fingerprint {
328 Some(value) => Some(value.as_str()),
329 None => None,
330 }
331 }
332}
333
334#[derive(Clone, Debug, Eq, PartialEq)]
339pub struct StableMemoryAllocation {
340 memory_id: u8,
341 stable_key: String,
342 schema_metadata: StableMemoryAllocationMetadata,
343}
344
345impl StableMemoryAllocation {
346 #[must_use]
348 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
349 Self::with_schema_metadata(
350 memory_id,
351 stable_key,
352 StableMemoryAllocationMetadata::absent(),
353 )
354 }
355
356 #[must_use]
361 pub const fn with_schema_metadata(
362 memory_id: u8,
363 stable_key: String,
364 schema_metadata: StableMemoryAllocationMetadata,
365 ) -> Self {
366 Self {
367 memory_id,
368 stable_key,
369 schema_metadata,
370 }
371 }
372
373 #[must_use]
375 pub const fn memory_id(&self) -> u8 {
376 self.memory_id
377 }
378
379 #[must_use]
381 pub const fn stable_key(&self) -> &str {
382 self.stable_key.as_str()
383 }
384
385 #[must_use]
387 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
388 &self.schema_metadata
389 }
390
391 #[must_use]
393 pub const fn schema_version(&self) -> Option<u32> {
394 self.schema_metadata.schema_version()
395 }
396
397 #[must_use]
399 pub const fn schema_fingerprint(&self) -> Option<&str> {
400 self.schema_metadata.schema_fingerprint()
401 }
402
403 #[must_use]
408 pub fn same_identity_as(&self, other: &Self) -> bool {
409 self.memory_id == other.memory_id && self.stable_key == other.stable_key
410 }
411}
412
413#[must_use]
414pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
415 format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
416}
417
418impl MacroNode for Store {
419 fn as_any(&self) -> &dyn std::any::Any {
420 self
421 }
422}
423
424impl ValidateNode for Store {
425 fn validate(&self) -> Result<(), ErrorTree> {
426 let mut errs = ErrorTree::new();
427 let schema = schema_read();
428
429 match schema.cast_node::<Canister>(self.canister()) {
430 Ok(canister) => {
431 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
432 match self.storage() {
433 StoreStorage::Stable(config) => {
434 validate_stable_memory_config(&mut errs, self, *config, canister);
435 }
436 }
437 }
438 Err(e) => errs.add(e),
439 }
440
441 errs.result()
442 }
443}
444
445fn validate_stable_memory_config(
446 errs: &mut ErrorTree,
447 store: &Store,
448 config: StoreStableMemoryConfig,
449 canister: &Canister,
450) {
451 validate_stable_memory_role(
452 errs,
453 "data_memory_id",
454 "data stable key",
455 config.data_memory_id(),
456 store
457 .stable_data_allocation(canister.memory_namespace())
458 .stable_key(),
459 canister,
460 );
461 validate_stable_memory_role(
462 errs,
463 "index_memory_id",
464 "index stable key",
465 config.index_memory_id(),
466 store
467 .stable_index_allocation(canister.memory_namespace())
468 .stable_key(),
469 canister,
470 );
471 validate_stable_memory_role(
472 errs,
473 "schema_memory_id",
474 "schema stable key",
475 config.schema_memory_id(),
476 store
477 .stable_schema_allocation(canister.memory_namespace())
478 .stable_key(),
479 canister,
480 );
481
482 if config.data_memory_id() == config.index_memory_id() {
483 err!(
484 errs,
485 "data_memory_id and index_memory_id must differ (both are {})",
486 config.data_memory_id(),
487 );
488 }
489 if config.data_memory_id() == config.schema_memory_id() {
490 err!(
491 errs,
492 "data_memory_id and schema_memory_id must differ (both are {})",
493 config.data_memory_id(),
494 );
495 }
496 if config.index_memory_id() == config.schema_memory_id() {
497 err!(
498 errs,
499 "index_memory_id and schema_memory_id must differ (both are {})",
500 config.index_memory_id(),
501 );
502 }
503}
504
505fn validate_stable_memory_role(
506 errs: &mut ErrorTree,
507 memory_label: &str,
508 stable_key_label: &str,
509 memory_id: u8,
510 stable_key: &str,
511 canister: &Canister,
512) {
513 validate_memory_id_in_range(
514 errs,
515 memory_label,
516 memory_id,
517 canister.memory_min(),
518 canister.memory_max(),
519 );
520 validate_app_memory_id(errs, memory_label, memory_id);
521 validate_memory_id_not_reserved(errs, memory_label, memory_id);
522 validate_stable_key(errs, stable_key_label, stable_key);
523}
524
525impl VisitableNode for Store {
526 fn route_key(&self) -> String {
527 self.def().path()
528 }
529
530 fn drive<V: Visitor>(&self, v: &mut V) {
531 self.def().accept(v);
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use crate::{
538 build::schema_write,
539 node::{Canister, SchemaNode},
540 };
541
542 use super::*;
543
544 fn insert_canister(path_module: &'static str, ident: &'static str) {
545 schema_write().insert_node(SchemaNode::Canister(Canister::new(
546 Def::new(path_module, ident),
547 "test_db",
548 100,
549 254,
550 254,
551 )));
552 }
553
554 #[test]
555 fn store_stable_keys_use_durable_icydb_shape() {
556 let store = Store::new_stable(
557 Def::new("demo::rpg", "CharacterStore"),
558 "CHARACTER_STORE",
559 "characters",
560 "demo::rpg::Canister",
561 StoreStableMemoryConfig::new(110, 111, 112),
562 );
563
564 assert_eq!(
565 store.stable_data_allocation("demo_rpg").stable_key(),
566 "icydb.demo_rpg.characters.data.v1",
567 );
568 assert_eq!(
569 store.stable_index_allocation("demo_rpg").stable_key(),
570 "icydb.demo_rpg.characters.index.v1",
571 );
572 assert_eq!(
573 store.stable_schema_allocation("demo_rpg").stable_key(),
574 "icydb.demo_rpg.characters.schema.v1",
575 );
576 }
577
578 #[test]
579 fn store_allocations_default_to_absent_schema_metadata() {
580 let store = Store::new_stable(
581 Def::new("demo::rpg", "CharacterStore"),
582 "CHARACTER_STORE",
583 "characters",
584 "demo::rpg::Canister",
585 StoreStableMemoryConfig::new(110, 111, 112),
586 );
587
588 for allocation in [
589 store.stable_data_allocation("demo_rpg"),
590 store.stable_index_allocation("demo_rpg"),
591 store.stable_schema_allocation("demo_rpg"),
592 ] {
593 assert_eq!(allocation.schema_version(), None);
594 assert_eq!(allocation.schema_fingerprint(), None);
595 assert_eq!(
596 allocation.schema_metadata(),
597 &StableMemoryAllocationMetadata::absent()
598 );
599 }
600 }
601
602 #[test]
603 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
604 let store = Store::new_stable(
605 Def::new("demo::rpg", "CharacterStore"),
606 "CHARACTER_STORE",
607 "characters",
608 "demo::rpg::Canister",
609 StoreStableMemoryConfig::new(110, 111, 112),
610 );
611 let data = store.stable_data_allocation_with_schema_metadata(
612 "demo_rpg",
613 StableMemoryAllocationMetadata::from_accepted_schema_contract(
614 7,
615 "data-row-layout".to_string(),
616 ),
617 );
618 let index = store.stable_index_allocation_with_schema_metadata(
619 "demo_rpg",
620 StableMemoryAllocationMetadata::from_accepted_schema_contract(
621 8,
622 "index-catalog".to_string(),
623 ),
624 );
625 let schema = store.stable_schema_allocation_with_schema_metadata(
626 "demo_rpg",
627 StableMemoryAllocationMetadata::from_accepted_schema_contract(
628 10,
629 "schema-catalog".to_string(),
630 ),
631 );
632 let data_after_reconcile = store.stable_data_allocation_with_schema_metadata(
633 "demo_rpg",
634 StableMemoryAllocationMetadata::from_accepted_schema_contract(
635 9,
636 "data-row-layout-v2".to_string(),
637 ),
638 );
639
640 assert_eq!(data.schema_version(), Some(7));
641 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
642 assert_eq!(index.schema_version(), Some(8));
643 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
644 assert_eq!(schema.schema_version(), Some(10));
645 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
646 assert!(data.same_identity_as(&data_after_reconcile));
647 assert!(!data.same_identity_as(&index));
648 assert!(!data.same_identity_as(&schema));
649 }
650
651 #[test]
652 fn store_owns_explicit_stable_storage_config() {
653 let store = Store::new_stable(
654 Def::new("demo::rpg", "CharacterStore"),
655 "CHARACTER_STORE",
656 "characters",
657 "demo::rpg::Canister",
658 StoreStableMemoryConfig::new(110, 111, 112),
659 );
660
661 assert!(store.is_stable_storage());
662 assert!(store.storage().stable_memory_config().is_some());
663 let stable = store
664 .stable_memory_config()
665 .expect("0.167 model stores stable config explicitly");
666
667 assert_eq!(stable.data_memory_id(), 110);
668 assert_eq!(stable.index_memory_id(), 111);
669 assert_eq!(stable.schema_memory_id(), 112);
670 assert_eq!(store.stable_data_memory_id(), 110);
671 assert_eq!(store.stable_index_memory_id(), 111);
672 assert_eq!(store.stable_schema_memory_id(), 112);
673 }
674
675 #[test]
676 fn store_stable_storage_config_rejects_duplicate_role_memory_ids() {
677 insert_canister("store_duplicate_role_memory_ids", "Canister");
678 let store = Store::new_stable(
679 Def::new("store_duplicate_role_memory_ids", "Store"),
680 "STORE",
681 "duplicate_role_memory_ids",
682 "store_duplicate_role_memory_ids::Canister",
683 StoreStableMemoryConfig::new(110, 110, 112),
684 );
685
686 let err = store
687 .validate()
688 .expect_err("duplicate store role memory IDs must fail validation");
689 let rendered = err.to_string();
690
691 assert!(
692 rendered.contains("data_memory_id and index_memory_id must differ"),
693 "expected duplicate role memory-id error, got: {rendered}"
694 );
695 }
696}