1use crate::{
7 db::schema::{
8 PersistedSchemaSnapshot, SchemaVersion, decode_persisted_schema_snapshot,
9 encode_persisted_schema_snapshot,
10 },
11 error::InternalError,
12 traits::Storable,
13 types::EntityTag,
14};
15use canic_cdk::structures::{BTreeMap, DefaultMemoryImpl, memory::VirtualMemory, storable::Bound};
16use std::borrow::Cow;
17
18const SCHEMA_KEY_BYTES_USIZE: usize = 12;
19const SCHEMA_KEY_BYTES: u32 = 12;
20const MAX_SCHEMA_SNAPSHOT_BYTES: u32 = 512 * 1024;
21
22#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
31struct RawSchemaKey([u8; SCHEMA_KEY_BYTES_USIZE]);
32
33#[allow(
34 dead_code,
35 reason = "raw schema keys are populated by upcoming startup reconciliation"
36)]
37impl RawSchemaKey {
38 #[must_use]
40 fn from_entity_version(entity: EntityTag, version: SchemaVersion) -> Self {
41 let mut out = [0u8; SCHEMA_KEY_BYTES_USIZE];
42 out[..size_of::<u64>()].copy_from_slice(&entity.value().to_be_bytes());
43 out[size_of::<u64>()..].copy_from_slice(&version.get().to_be_bytes());
44
45 Self(out)
46 }
47
48 #[must_use]
50 fn entity_tag(self) -> EntityTag {
51 let mut bytes = [0u8; size_of::<u64>()];
52 bytes.copy_from_slice(&self.0[..size_of::<u64>()]);
53
54 EntityTag::new(u64::from_be_bytes(bytes))
55 }
56
57 #[must_use]
59 fn version(self) -> u32 {
60 let mut bytes = [0u8; size_of::<u32>()];
61 bytes.copy_from_slice(&self.0[size_of::<u64>()..]);
62
63 u32::from_be_bytes(bytes)
64 }
65}
66
67impl Storable for RawSchemaKey {
68 fn to_bytes(&self) -> Cow<'_, [u8]> {
69 Cow::Borrowed(&self.0)
70 }
71
72 fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
73 debug_assert_eq!(
74 bytes.len(),
75 SCHEMA_KEY_BYTES_USIZE,
76 "RawSchemaKey::from_bytes received unexpected byte length",
77 );
78
79 if bytes.len() != SCHEMA_KEY_BYTES_USIZE {
80 return Self([0u8; SCHEMA_KEY_BYTES_USIZE]);
81 }
82
83 let mut out = [0u8; SCHEMA_KEY_BYTES_USIZE];
84 out.copy_from_slice(bytes.as_ref());
85 Self(out)
86 }
87
88 fn into_bytes(self) -> Vec<u8> {
89 self.0.to_vec()
90 }
91
92 const BOUND: Bound = Bound::Bounded {
93 max_size: SCHEMA_KEY_BYTES,
94 is_fixed_size: true,
95 };
96}
97
98#[derive(Clone, Debug, Eq, PartialEq)]
108struct RawSchemaSnapshot(Vec<u8>);
109
110impl RawSchemaSnapshot {
111 fn from_persisted_snapshot(snapshot: &PersistedSchemaSnapshot) -> Result<Self, InternalError> {
113 encode_persisted_schema_snapshot(snapshot).map(Self)
114 }
115
116 #[must_use]
118 #[cfg(test)]
119 const fn from_bytes(bytes: Vec<u8>) -> Self {
120 Self(bytes)
121 }
122
123 #[must_use]
125 const fn as_bytes(&self) -> &[u8] {
126 self.0.as_slice()
127 }
128
129 #[must_use]
131 #[cfg(test)]
132 fn into_bytes(self) -> Vec<u8> {
133 self.0
134 }
135
136 fn decode_persisted_snapshot(&self) -> Result<PersistedSchemaSnapshot, InternalError> {
138 decode_persisted_schema_snapshot(self.as_bytes())
139 }
140}
141
142impl Storable for RawSchemaSnapshot {
143 fn to_bytes(&self) -> Cow<'_, [u8]> {
144 Cow::Borrowed(self.as_bytes())
145 }
146
147 fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
148 Self(bytes.into_owned())
149 }
150
151 fn into_bytes(self) -> Vec<u8> {
152 self.0
153 }
154
155 const BOUND: Bound = Bound::Bounded {
156 max_size: MAX_SCHEMA_SNAPSHOT_BYTES,
157 is_fixed_size: false,
158 };
159}
160
161pub struct SchemaStore {
170 map: BTreeMap<RawSchemaKey, RawSchemaSnapshot, VirtualMemory<DefaultMemoryImpl>>,
171}
172
173impl SchemaStore {
174 #[must_use]
176 pub fn init(memory: VirtualMemory<DefaultMemoryImpl>) -> Self {
177 Self {
178 map: BTreeMap::init(memory),
179 }
180 }
181
182 pub(in crate::db) fn insert_persisted_snapshot(
184 &mut self,
185 entity: EntityTag,
186 snapshot: &PersistedSchemaSnapshot,
187 ) -> Result<(), InternalError> {
188 let key = RawSchemaKey::from_entity_version(entity, snapshot.version());
189 let raw_snapshot = RawSchemaSnapshot::from_persisted_snapshot(snapshot)?;
190 let _ = self.insert_raw_snapshot(key, raw_snapshot);
191
192 Ok(())
193 }
194
195 pub(in crate::db) fn get_persisted_snapshot(
197 &self,
198 entity: EntityTag,
199 version: SchemaVersion,
200 ) -> Result<Option<PersistedSchemaSnapshot>, InternalError> {
201 let key = RawSchemaKey::from_entity_version(entity, version);
202 self.get_raw_snapshot(&key)
203 .map(|snapshot| snapshot.decode_persisted_snapshot())
204 .transpose()
205 }
206
207 fn insert_raw_snapshot(
209 &mut self,
210 key: RawSchemaKey,
211 snapshot: RawSchemaSnapshot,
212 ) -> Option<RawSchemaSnapshot> {
213 self.map.insert(key, snapshot)
214 }
215
216 #[must_use]
218 fn get_raw_snapshot(&self, key: &RawSchemaKey) -> Option<RawSchemaSnapshot> {
219 self.map.get(key)
220 }
221
222 #[must_use]
224 #[cfg(test)]
225 fn contains_raw_snapshot(&self, key: &RawSchemaKey) -> bool {
226 self.map.contains_key(key)
227 }
228
229 #[must_use]
231 #[cfg(test)]
232 pub(in crate::db) fn len(&self) -> u64 {
233 self.map.len()
234 }
235
236 #[must_use]
238 #[cfg(test)]
239 pub(in crate::db) fn is_empty(&self) -> bool {
240 self.map.is_empty()
241 }
242
243 #[cfg(test)]
245 pub(in crate::db) fn clear(&mut self) {
246 self.map.clear();
247 }
248}
249
250#[cfg(test)]
255mod tests {
256 use super::{RawSchemaKey, RawSchemaSnapshot, SchemaStore};
257 use crate::{
258 db::schema::{
259 FieldId, PersistedFieldKind, PersistedFieldSnapshot, PersistedSchemaSnapshot,
260 SchemaFieldDefault, SchemaFieldSlot, SchemaRowLayout, SchemaVersion,
261 },
262 model::field::{FieldStorageDecode, LeafCodec, ScalarCodec},
263 testing::test_memory,
264 traits::Storable,
265 types::EntityTag,
266 };
267 use std::borrow::Cow;
268
269 #[test]
270 fn raw_schema_key_round_trips_entity_and_version() {
271 let key = RawSchemaKey::from_entity_version(EntityTag::new(0x0102_0304_0506_0708), {
272 SchemaVersion::initial()
273 });
274 let encoded = key.to_bytes().into_owned();
275 let decoded = RawSchemaKey::from_bytes(Cow::Owned(encoded));
276
277 assert_eq!(decoded.entity_tag(), EntityTag::new(0x0102_0304_0506_0708));
278 assert_eq!(decoded.version(), SchemaVersion::initial().get());
279 }
280
281 #[test]
282 fn raw_schema_snapshot_round_trips_payload_bytes() {
283 let snapshot = RawSchemaSnapshot::from_bytes(vec![1, 2, 3, 5, 8]);
284 let encoded = snapshot.to_bytes().into_owned();
285 let decoded = <RawSchemaSnapshot as Storable>::from_bytes(Cow::Owned(encoded));
286
287 assert_eq!(decoded.as_bytes(), &[1, 2, 3, 5, 8]);
288 assert_eq!(decoded.into_bytes(), vec![1, 2, 3, 5, 8]);
289 }
290
291 #[test]
292 fn schema_store_persists_raw_snapshots_by_entity_version_key() {
293 let mut store = SchemaStore::init(test_memory(251));
294 let key = RawSchemaKey::from_entity_version(EntityTag::new(17), SchemaVersion::initial());
295
296 assert!(store.is_empty());
297 assert!(!store.contains_raw_snapshot(&key));
298
299 store.insert_raw_snapshot(key, RawSchemaSnapshot::from_bytes(vec![9, 4, 6]));
300
301 assert_eq!(store.len(), 1);
302 assert!(store.contains_raw_snapshot(&key));
303 assert_eq!(
304 store
305 .get_raw_snapshot(&key)
306 .expect("schema snapshot should be present")
307 .as_bytes(),
308 &[9, 4, 6],
309 );
310
311 store.clear();
312 assert!(store.is_empty());
313 }
314
315 #[test]
316 fn raw_schema_snapshot_encodes_and_decodes_typed_snapshot() {
317 let snapshot = PersistedSchemaSnapshot::new(
318 SchemaVersion::initial(),
319 "entities::Encoded".to_string(),
320 "Encoded".to_string(),
321 FieldId::new(1),
322 SchemaRowLayout::new(
323 SchemaVersion::initial(),
324 vec![
325 (FieldId::new(1), SchemaFieldSlot::new(0)),
326 (FieldId::new(2), SchemaFieldSlot::new(1)),
327 ],
328 ),
329 vec![
330 PersistedFieldSnapshot::new(
331 FieldId::new(1),
332 "id".to_string(),
333 SchemaFieldSlot::new(0),
334 PersistedFieldKind::Ulid,
335 false,
336 SchemaFieldDefault::None,
337 FieldStorageDecode::ByKind,
338 LeafCodec::Scalar(ScalarCodec::Ulid),
339 ),
340 PersistedFieldSnapshot::new(
341 FieldId::new(2),
342 "payload".to_string(),
343 SchemaFieldSlot::new(1),
344 PersistedFieldKind::Map {
345 key: Box::new(PersistedFieldKind::Text { max_len: None }),
346 value: Box::new(PersistedFieldKind::List(Box::new(
347 PersistedFieldKind::Uint,
348 ))),
349 },
350 false,
351 SchemaFieldDefault::None,
352 FieldStorageDecode::ByKind,
353 LeafCodec::StructuralFallback,
354 ),
355 ],
356 );
357
358 let raw = RawSchemaSnapshot::from_persisted_snapshot(&snapshot)
359 .expect("schema snapshot should encode");
360 let decoded = raw
361 .decode_persisted_snapshot()
362 .expect("schema snapshot should decode");
363
364 assert_eq!(decoded, snapshot);
365 }
366}