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 data_memory_id: u8,
23 index_memory_id: u8,
24 schema_memory_id: u8,
25}
26
27impl Store {
28 #[must_use]
29 pub const fn new(
30 def: Def,
31 ident: &'static str,
32 store_name: &'static str,
33 canister: &'static str,
34 data_memory_id: u8,
35 index_memory_id: u8,
36 schema_memory_id: u8,
37 ) -> Self {
38 Self {
39 def,
40 ident,
41 name: store_name,
42 canister,
43 data_memory_id,
44 index_memory_id,
45 schema_memory_id,
46 }
47 }
48
49 #[must_use]
50 pub const fn def(&self) -> &Def {
51 &self.def
52 }
53
54 #[must_use]
55 pub const fn ident(&self) -> &'static str {
56 self.ident
57 }
58
59 #[must_use]
60 pub const fn store_name(&self) -> &'static str {
61 self.name
62 }
63
64 #[must_use]
65 pub const fn canister(&self) -> &'static str {
66 self.canister
67 }
68
69 #[must_use]
70 pub const fn data_memory_id(&self) -> u8 {
71 self.data_memory_id
72 }
73
74 #[must_use]
75 pub const fn index_memory_id(&self) -> u8 {
76 self.index_memory_id
77 }
78
79 #[must_use]
80 pub const fn schema_memory_id(&self) -> u8 {
81 self.schema_memory_id
82 }
83
84 #[must_use]
85 pub fn data_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
86 self.allocation(memory_namespace, StoreMemoryRole::Data)
87 }
88
89 #[must_use]
92 pub fn data_allocation_with_schema_metadata(
93 &self,
94 memory_namespace: &str,
95 schema_metadata: StableMemoryAllocationMetadata,
96 ) -> StableMemoryAllocation {
97 self.allocation_with_schema_metadata(
98 memory_namespace,
99 StoreMemoryRole::Data,
100 schema_metadata,
101 )
102 }
103
104 #[must_use]
105 pub fn index_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
106 self.allocation(memory_namespace, StoreMemoryRole::Index)
107 }
108
109 #[must_use]
112 pub fn index_allocation_with_schema_metadata(
113 &self,
114 memory_namespace: &str,
115 schema_metadata: StableMemoryAllocationMetadata,
116 ) -> StableMemoryAllocation {
117 self.allocation_with_schema_metadata(
118 memory_namespace,
119 StoreMemoryRole::Index,
120 schema_metadata,
121 )
122 }
123
124 #[must_use]
125 pub fn schema_allocation(&self, memory_namespace: &str) -> StableMemoryAllocation {
126 self.allocation(memory_namespace, StoreMemoryRole::Schema)
127 }
128
129 #[must_use]
132 pub fn schema_allocation_with_schema_metadata(
133 &self,
134 memory_namespace: &str,
135 schema_metadata: StableMemoryAllocationMetadata,
136 ) -> StableMemoryAllocation {
137 self.allocation_with_schema_metadata(
138 memory_namespace,
139 StoreMemoryRole::Schema,
140 schema_metadata,
141 )
142 }
143
144 #[must_use]
145 pub fn allocation(
146 &self,
147 memory_namespace: &str,
148 role: StoreMemoryRole,
149 ) -> StableMemoryAllocation {
150 let memory_id = match role {
151 StoreMemoryRole::Data => self.data_memory_id,
152 StoreMemoryRole::Index => self.index_memory_id,
153 StoreMemoryRole::Schema => self.schema_memory_id,
154 };
155
156 StableMemoryAllocation::without_schema_metadata(
157 memory_id,
158 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
159 )
160 }
161
162 fn allocation_with_schema_metadata(
163 &self,
164 memory_namespace: &str,
165 role: StoreMemoryRole,
166 schema_metadata: StableMemoryAllocationMetadata,
167 ) -> StableMemoryAllocation {
168 let memory_id = match role {
169 StoreMemoryRole::Data => self.data_memory_id,
170 StoreMemoryRole::Index => self.index_memory_id,
171 StoreMemoryRole::Schema => self.schema_memory_id,
172 };
173
174 StableMemoryAllocation::with_schema_metadata(
175 memory_id,
176 stable_memory_key(memory_namespace, self.store_name(), role.as_str()),
177 schema_metadata,
178 )
179 }
180}
181
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub enum StoreMemoryRole {
184 Data,
185 Index,
186 Schema,
187}
188
189impl StoreMemoryRole {
190 #[must_use]
191 pub const fn as_str(self) -> &'static str {
192 match self {
193 Self::Data => "data",
194 Self::Index => "index",
195 Self::Schema => "schema",
196 }
197 }
198}
199
200#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct StableMemoryAllocationMetadata {
206 schema_version: Option<u32>,
207 schema_fingerprint: Option<String>,
208}
209
210impl StableMemoryAllocationMetadata {
211 const fn new(schema_version: Option<u32>, schema_fingerprint: Option<String>) -> Self {
212 Self {
213 schema_version,
214 schema_fingerprint,
215 }
216 }
217
218 #[must_use]
220 pub const fn from_accepted_schema_contract(
221 schema_version: u32,
222 schema_fingerprint: String,
223 ) -> Self {
224 Self::new(Some(schema_version), Some(schema_fingerprint))
225 }
226
227 #[must_use]
230 pub const fn absent() -> Self {
231 Self::new(None, None)
232 }
233
234 #[must_use]
236 pub const fn schema_version(&self) -> Option<u32> {
237 self.schema_version
238 }
239
240 #[must_use]
242 pub const fn schema_fingerprint(&self) -> Option<&str> {
243 match &self.schema_fingerprint {
244 Some(value) => Some(value.as_str()),
245 None => None,
246 }
247 }
248}
249
250#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct StableMemoryAllocation {
256 memory_id: u8,
257 stable_key: String,
258 schema_metadata: StableMemoryAllocationMetadata,
259}
260
261impl StableMemoryAllocation {
262 #[must_use]
264 pub const fn without_schema_metadata(memory_id: u8, stable_key: String) -> Self {
265 Self::with_schema_metadata(
266 memory_id,
267 stable_key,
268 StableMemoryAllocationMetadata::absent(),
269 )
270 }
271
272 #[must_use]
277 pub const fn with_schema_metadata(
278 memory_id: u8,
279 stable_key: String,
280 schema_metadata: StableMemoryAllocationMetadata,
281 ) -> Self {
282 Self {
283 memory_id,
284 stable_key,
285 schema_metadata,
286 }
287 }
288
289 #[must_use]
291 pub const fn memory_id(&self) -> u8 {
292 self.memory_id
293 }
294
295 #[must_use]
297 pub const fn stable_key(&self) -> &str {
298 self.stable_key.as_str()
299 }
300
301 #[must_use]
303 pub const fn schema_metadata(&self) -> &StableMemoryAllocationMetadata {
304 &self.schema_metadata
305 }
306
307 #[must_use]
309 pub const fn schema_version(&self) -> Option<u32> {
310 self.schema_metadata.schema_version()
311 }
312
313 #[must_use]
315 pub const fn schema_fingerprint(&self) -> Option<&str> {
316 self.schema_metadata.schema_fingerprint()
317 }
318
319 #[must_use]
324 pub fn same_identity_as(&self, other: &Self) -> bool {
325 self.memory_id == other.memory_id && self.stable_key == other.stable_key
326 }
327}
328
329#[must_use]
330pub fn stable_memory_key(memory_namespace: &str, store_name: &str, role: &str) -> String {
331 format!("icydb.{memory_namespace}.{store_name}.{role}.v1")
332}
333
334impl MacroNode for Store {
335 fn as_any(&self) -> &dyn std::any::Any {
336 self
337 }
338}
339
340impl ValidateNode for Store {
341 fn validate(&self) -> Result<(), ErrorTree> {
342 let mut errs = ErrorTree::new();
343 let schema = schema_read();
344
345 match schema.cast_node::<Canister>(self.canister()) {
346 Ok(canister) => {
347 validate_stable_key_segment(&mut errs, "store store_name", self.store_name());
348
349 validate_memory_id_in_range(
351 &mut errs,
352 "data_memory_id",
353 self.data_memory_id(),
354 canister.memory_min(),
355 canister.memory_max(),
356 );
357 validate_app_memory_id(&mut errs, "data_memory_id", self.data_memory_id());
358 validate_memory_id_not_reserved(&mut errs, "data_memory_id", self.data_memory_id());
359 validate_stable_key(
360 &mut errs,
361 "data stable key",
362 self.data_allocation(canister.memory_namespace())
363 .stable_key(),
364 );
365
366 validate_memory_id_in_range(
368 &mut errs,
369 "index_memory_id",
370 self.index_memory_id(),
371 canister.memory_min(),
372 canister.memory_max(),
373 );
374 validate_app_memory_id(&mut errs, "index_memory_id", self.index_memory_id());
375 validate_memory_id_not_reserved(
376 &mut errs,
377 "index_memory_id",
378 self.index_memory_id(),
379 );
380 validate_stable_key(
381 &mut errs,
382 "index stable key",
383 self.index_allocation(canister.memory_namespace())
384 .stable_key(),
385 );
386
387 validate_memory_id_in_range(
389 &mut errs,
390 "schema_memory_id",
391 self.schema_memory_id(),
392 canister.memory_min(),
393 canister.memory_max(),
394 );
395 validate_app_memory_id(&mut errs, "schema_memory_id", self.schema_memory_id());
396 validate_memory_id_not_reserved(
397 &mut errs,
398 "schema_memory_id",
399 self.schema_memory_id(),
400 );
401 validate_stable_key(
402 &mut errs,
403 "schema stable key",
404 self.schema_allocation(canister.memory_namespace())
405 .stable_key(),
406 );
407
408 if self.data_memory_id() == self.index_memory_id() {
410 err!(
411 errs,
412 "data_memory_id and index_memory_id must differ (both are {})",
413 self.data_memory_id(),
414 );
415 }
416 if self.data_memory_id() == self.schema_memory_id() {
417 err!(
418 errs,
419 "data_memory_id and schema_memory_id must differ (both are {})",
420 self.data_memory_id(),
421 );
422 }
423 if self.index_memory_id() == self.schema_memory_id() {
424 err!(
425 errs,
426 "index_memory_id and schema_memory_id must differ (both are {})",
427 self.index_memory_id(),
428 );
429 }
430 }
431 Err(e) => errs.add(e),
432 }
433
434 errs.result()
435 }
436}
437
438impl VisitableNode for Store {
439 fn route_key(&self) -> String {
440 self.def().path()
441 }
442
443 fn drive<V: Visitor>(&self, v: &mut V) {
444 self.def().accept(v);
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn store_stable_keys_use_durable_icydb_shape() {
454 let store = Store::new(
455 Def::new("demo::rpg", "CharacterStore"),
456 "CHARACTER_STORE",
457 "characters",
458 "demo::rpg::Canister",
459 110,
460 111,
461 112,
462 );
463
464 assert_eq!(
465 store.data_allocation("demo_rpg").stable_key(),
466 "icydb.demo_rpg.characters.data.v1",
467 );
468 assert_eq!(
469 store.index_allocation("demo_rpg").stable_key(),
470 "icydb.demo_rpg.characters.index.v1",
471 );
472 assert_eq!(
473 store.schema_allocation("demo_rpg").stable_key(),
474 "icydb.demo_rpg.characters.schema.v1",
475 );
476 }
477
478 #[test]
479 fn store_allocations_default_to_absent_schema_metadata() {
480 let store = Store::new(
481 Def::new("demo::rpg", "CharacterStore"),
482 "CHARACTER_STORE",
483 "characters",
484 "demo::rpg::Canister",
485 110,
486 111,
487 112,
488 );
489
490 for allocation in [
491 store.data_allocation("demo_rpg"),
492 store.index_allocation("demo_rpg"),
493 store.schema_allocation("demo_rpg"),
494 ] {
495 assert_eq!(allocation.schema_version(), None);
496 assert_eq!(allocation.schema_fingerprint(), None);
497 assert_eq!(
498 allocation.schema_metadata(),
499 &StableMemoryAllocationMetadata::absent()
500 );
501 }
502 }
503
504 #[test]
505 fn allocation_metadata_is_role_specific_and_diagnostic_only() {
506 let store = Store::new(
507 Def::new("demo::rpg", "CharacterStore"),
508 "CHARACTER_STORE",
509 "characters",
510 "demo::rpg::Canister",
511 110,
512 111,
513 112,
514 );
515 let data = store.data_allocation_with_schema_metadata(
516 "demo_rpg",
517 StableMemoryAllocationMetadata::from_accepted_schema_contract(
518 7,
519 "data-row-layout".to_string(),
520 ),
521 );
522 let index = store.index_allocation_with_schema_metadata(
523 "demo_rpg",
524 StableMemoryAllocationMetadata::from_accepted_schema_contract(
525 8,
526 "index-catalog".to_string(),
527 ),
528 );
529 let schema = store.schema_allocation_with_schema_metadata(
530 "demo_rpg",
531 StableMemoryAllocationMetadata::from_accepted_schema_contract(
532 10,
533 "schema-catalog".to_string(),
534 ),
535 );
536 let data_after_reconcile = store.data_allocation_with_schema_metadata(
537 "demo_rpg",
538 StableMemoryAllocationMetadata::from_accepted_schema_contract(
539 9,
540 "data-row-layout-v2".to_string(),
541 ),
542 );
543
544 assert_eq!(data.schema_version(), Some(7));
545 assert_eq!(data.schema_fingerprint(), Some("data-row-layout"));
546 assert_eq!(index.schema_version(), Some(8));
547 assert_eq!(index.schema_fingerprint(), Some("index-catalog"));
548 assert_eq!(schema.schema_version(), Some(10));
549 assert_eq!(schema.schema_fingerprint(), Some("schema-catalog"));
550 assert!(data.same_identity_as(&data_after_reconcile));
551 assert!(!data.same_identity_as(&index));
552 assert!(!data.same_identity_as(&schema));
553 }
554}