1use std::collections::HashMap;
19use std::collections::hash_map::Entry;
20use std::sync::{Arc, Mutex};
21
22use nodedb_array::schema::array_schema::ArraySchema;
23use nodedb_array::sync::hlc::Hlc;
24use nodedb_array::sync::schema_crdt::SchemaDoc;
25use nodedb_types::Namespace;
26
27use crate::error::LiteError;
28use crate::storage::engine::StorageEngine;
29use crate::sync::array::replica_state::ReplicaState;
30
31const SCHEMA_KEY_PREFIX: &str = "array.schema_doc:";
33
34#[derive(zerompk::ToMessagePack, zerompk::FromMessagePack)]
36struct PersistedSchema {
37 replica_id: u64,
38 schema_hlc_bytes: Vec<u8>,
39 loro_snapshot: Vec<u8>,
40}
41
42fn schema_key(name: &str) -> Vec<u8> {
44 format!("{SCHEMA_KEY_PREFIX}{name}").into_bytes()
45}
46
47pub struct SchemaRegistry<S: StorageEngine> {
52 storage: Arc<S>,
53 docs: Mutex<HashMap<String, SchemaDoc>>,
54 replica: Arc<ReplicaState>,
55 write_lock: tokio::sync::Mutex<()>,
61}
62
63impl<S: StorageEngine> SchemaRegistry<S> {
64 pub fn new(storage: Arc<S>, replica: Arc<ReplicaState>) -> Self {
68 Self {
69 storage,
70 docs: Mutex::new(HashMap::new()),
71 replica,
72 write_lock: tokio::sync::Mutex::new(()),
73 }
74 }
75
76 pub async fn load(storage: Arc<S>, replica: Arc<ReplicaState>) -> Result<Self, LiteError> {
80 let prefix = SCHEMA_KEY_PREFIX.as_bytes();
81 let pairs = storage.scan_prefix(Namespace::Meta, prefix).await?;
82
83 let mut docs = HashMap::new();
84 for (key, value) in pairs {
85 let name_bytes = &key[prefix.len()..];
86 let name = std::str::from_utf8(name_bytes).map_err(|e| LiteError::Storage {
87 detail: format!("schema_registry: non-UTF8 array name in storage: {e}"),
88 })?;
89
90 let persisted: PersistedSchema =
91 zerompk::from_msgpack(&value).map_err(|e| LiteError::Storage {
92 detail: format!("schema_registry: decode PersistedSchema for '{name}': {e}"),
93 })?;
94
95 let hlc_arr: [u8; 18] =
96 persisted
97 .schema_hlc_bytes
98 .try_into()
99 .map_err(|v: Vec<u8>| LiteError::Storage {
100 detail: format!(
101 "schema_registry: schema_hlc_bytes wrong length ({}) for '{name}'",
102 v.len()
103 ),
104 })?;
105 let schema_hlc = Hlc::from_bytes(&hlc_arr);
106
107 let mut doc = SchemaDoc::new(replica.replica_id());
108 doc.import_snapshot(&persisted.loro_snapshot, schema_hlc, &replica.hlc_gen())
109 .map_err(|e| LiteError::Storage {
110 detail: format!("schema_registry: loro import for '{name}': {e}"),
111 })?;
112
113 docs.insert(name.to_owned(), doc);
114 }
115
116 Ok(Self {
117 storage,
118 docs: Mutex::new(docs),
119 replica,
120 write_lock: tokio::sync::Mutex::new(()),
121 })
122 }
123
124 pub async fn put_schema(&self, name: &str, schema: &ArraySchema) -> Result<Hlc, LiteError> {
129 let _write = self.write_lock.lock().await;
130 let (schema_hlc, snapshot) = {
131 let mut docs = self.docs.lock().map_err(|_| LiteError::LockPoisoned)?;
132
133 let doc = match docs.entry(name.to_owned()) {
134 Entry::Occupied(entry) => {
135 let existing = entry.into_mut();
136 existing
137 .replace_schema(schema, &self.replica.hlc_gen())
138 .map_err(|e| LiteError::Storage {
139 detail: format!("schema_registry put_schema '{name}': {e}"),
140 })?;
141 existing
142 }
143 Entry::Vacant(entry) => {
144 let new_doc = SchemaDoc::from_schema(
145 self.replica.replica_id(),
146 schema,
147 &self.replica.hlc_gen(),
148 )
149 .map_err(|e| LiteError::Storage {
150 detail: format!("schema_registry from_schema '{name}': {e}"),
151 })?;
152 entry.insert(new_doc)
153 }
154 };
155
156 let schema_hlc = doc.schema_hlc();
157 let snapshot = doc.export_snapshot().map_err(|e| LiteError::Storage {
158 detail: format!("schema_registry export '{name}': {e}"),
159 })?;
160 (schema_hlc, snapshot)
161 };
162
163 self.persist(name, schema_hlc, snapshot).await?;
164 Ok(schema_hlc)
165 }
166
167 pub fn schema_hlc(&self, name: &str) -> Option<Hlc> {
169 let docs = self.docs.lock().ok()?;
170 docs.get(name).map(|d| d.schema_hlc())
171 }
172
173 pub async fn import_snapshot(
178 &self,
179 name: &str,
180 snapshot_bytes: &[u8],
181 remote_hlc: Hlc,
182 ) -> Result<(), LiteError> {
183 let _write = self.write_lock.lock().await;
184 let (schema_hlc, snapshot) = {
185 let mut docs = self.docs.lock().map_err(|_| LiteError::LockPoisoned)?;
186
187 let doc = docs
188 .entry(name.to_owned())
189 .or_insert_with(|| SchemaDoc::new(self.replica.replica_id()));
190
191 doc.import_snapshot(snapshot_bytes, remote_hlc, &self.replica.hlc_gen())
192 .map_err(|e| LiteError::Storage {
193 detail: format!("schema_registry import_snapshot '{name}': {e}"),
194 })?;
195
196 let schema_hlc = doc.schema_hlc();
197 let snapshot = doc.export_snapshot().map_err(|e| LiteError::Storage {
198 detail: format!("schema_registry export after import '{name}': {e}"),
199 })?;
200 (schema_hlc, snapshot)
201 };
202
203 self.persist(name, schema_hlc, snapshot).await
204 }
205
206 pub fn list_arrays(&self) -> Vec<String> {
208 self.docs
209 .lock()
210 .ok()
211 .map(|docs| docs.keys().cloned().collect())
212 .unwrap_or_default()
213 }
214
215 pub fn export_snapshot(&self, name: &str) -> Result<Option<Vec<u8>>, LiteError> {
219 let docs = self.docs.lock().map_err(|_| LiteError::LockPoisoned)?;
220 if let Some(doc) = docs.get(name) {
221 let bytes = doc.export_snapshot().map_err(|e| LiteError::Storage {
222 detail: format!("schema_registry export_snapshot '{name}': {e}"),
223 })?;
224 Ok(Some(bytes))
225 } else {
226 Ok(None)
227 }
228 }
229
230 pub fn export_versioned(&self, name: &str) -> Result<Option<(Hlc, Vec<u8>)>, LiteError> {
235 let docs = self.docs.lock().map_err(|_| LiteError::LockPoisoned)?;
236 let Some(doc) = docs.get(name) else {
237 return Ok(None);
238 };
239 let bytes = doc.export_snapshot().map_err(|e| LiteError::Storage {
240 detail: format!("schema_registry export_versioned '{name}': {e}"),
241 })?;
242 Ok(Some((doc.schema_hlc(), bytes)))
243 }
244
245 async fn persist(
248 &self,
249 name: &str,
250 schema_hlc: Hlc,
251 loro_snapshot: Vec<u8>,
252 ) -> Result<(), LiteError> {
253 let persisted = PersistedSchema {
254 replica_id: self.replica.replica_id().as_u64(),
255 schema_hlc_bytes: schema_hlc.to_bytes().to_vec(),
256 loro_snapshot,
257 };
258 let bytes = zerompk::to_msgpack_vec(&persisted).map_err(|e| LiteError::Serialization {
259 detail: format!("schema_registry persist '{name}': {e}"),
260 })?;
261 self.storage
262 .put(Namespace::Meta, &schema_key(name), &bytes)
263 .await
264 }
265}
266
267#[cfg(test)]
270mod tests {
271 use super::*;
272 use crate::storage::pagedb_storage::{PagedbStorageDefault, PagedbStorageMem};
273 use nodedb_array::schema::array_schema::ArraySchema;
274 use nodedb_array::schema::attr_spec::{AttrSpec, AttrType};
275 use nodedb_array::schema::cell_order::{CellOrder, TileOrder};
276 use nodedb_array::schema::dim_spec::{DimSpec, DimType};
277 use nodedb_array::sync::replica_id::ReplicaId;
278 use nodedb_array::types::domain::{Domain, DomainBound};
279
280 fn simple_schema(name: &str) -> ArraySchema {
281 ArraySchema {
282 name: name.into(),
283 dims: vec![DimSpec::new(
284 "x",
285 DimType::Int64,
286 Domain::new(DomainBound::Int64(0), DomainBound::Int64(99)),
287 )],
288 attrs: vec![AttrSpec::new("v", AttrType::Float64, true)],
289 tile_extents: vec![10],
290 cell_order: CellOrder::RowMajor,
291 tile_order: TileOrder::RowMajor,
292 }
293 }
294
295 use crate::storage::engine::StorageEngine;
296
297 async fn make_replica(storage: &Arc<PagedbStorageMem>) -> Arc<ReplicaState> {
298 Arc::new(ReplicaState::load_or_init(&**storage).await.unwrap())
299 }
300
301 async fn make_registry(storage: Arc<PagedbStorageMem>) -> SchemaRegistry<PagedbStorageMem> {
302 let replica = make_replica(&storage).await;
303 SchemaRegistry::new(storage, replica)
304 }
305
306 #[tokio::test]
307 async fn put_schema_persists() {
308 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
309 let reg = make_registry(Arc::clone(&storage)).await;
310 let hlc = reg.put_schema("arr", &simple_schema("arr")).await.unwrap();
311 assert!(hlc > Hlc::ZERO);
312
313 let raw = storage
315 .get(Namespace::Meta, &schema_key("arr"))
316 .await
317 .unwrap();
318 assert!(raw.is_some(), "schema must be persisted");
319 }
320
321 #[tokio::test]
322 async fn load_restores_after_restart() {
323 let dir = tempfile::tempdir().unwrap();
324 let path = dir.path().join("schema_reg.pagedb");
325
326 let schema_hlc;
327 {
328 let storage = Arc::new(
329 PagedbStorageDefault::open(
330 &path,
331 crate::storage::encryption::Encryption::Plaintext,
332 )
333 .await
334 .unwrap(),
335 );
336 let replica = Arc::new(ReplicaState::load_or_init(&*storage).await.unwrap());
337 let reg = SchemaRegistry::new(Arc::clone(&storage), Arc::clone(&replica));
338 schema_hlc = reg.put_schema("arr", &simple_schema("arr")).await.unwrap();
339 }
340
341 {
342 let storage = Arc::new(
343 PagedbStorageDefault::open(
344 &path,
345 crate::storage::encryption::Encryption::Plaintext,
346 )
347 .await
348 .unwrap(),
349 );
350 let replica = Arc::new(ReplicaState::load_or_init(&*storage).await.unwrap());
351 let reg = SchemaRegistry::load(Arc::clone(&storage), replica)
352 .await
353 .unwrap();
354 let loaded_hlc = reg.schema_hlc("arr").expect("arr must be loaded");
355 assert!(loaded_hlc >= schema_hlc);
357 }
358 }
359
360 #[tokio::test]
361 async fn import_snapshot_creates_entry() {
362 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
363 let replica = make_replica(&storage).await;
364
365 let reg_a = SchemaRegistry::new(Arc::clone(&storage), Arc::clone(&replica));
367 reg_a.put_schema("x", &simple_schema("x")).await.unwrap();
368 let snapshot = reg_a.export_snapshot("x").unwrap().unwrap();
369 let remote_hlc = reg_a.schema_hlc("x").unwrap();
370
371 let storage_b = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
373 let replica_b = make_replica(&storage_b).await;
374 let reg_b = SchemaRegistry::new(storage_b, replica_b);
375
376 assert!(reg_b.schema_hlc("x").is_none(), "not yet present");
377 reg_b
378 .import_snapshot("x", &snapshot, remote_hlc)
379 .await
380 .unwrap();
381 assert!(reg_b.schema_hlc("x").is_some(), "must exist after import");
382 }
383
384 #[tokio::test]
385 async fn import_snapshot_advances_hlc() {
386 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
387 let replica = make_replica(&storage).await;
388 let reg = SchemaRegistry::new(Arc::clone(&storage), Arc::clone(&replica));
389
390 reg.put_schema("y", &simple_schema("y")).await.unwrap();
392 let hlc_before = reg.schema_hlc("y").unwrap();
393
394 let snapshot = reg.export_snapshot("y").unwrap().unwrap();
396 let future_hlc = Hlc::new(hlc_before.physical_ms + 50_000, 0, ReplicaId::new(99)).unwrap();
397 reg.import_snapshot("y", &snapshot, future_hlc)
398 .await
399 .unwrap();
400
401 let hlc_after = reg.schema_hlc("y").unwrap();
402 assert!(hlc_after > hlc_before, "HLC must advance on import");
403 }
404
405 #[tokio::test]
406 async fn export_returns_none_for_unknown() {
407 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
408 let reg = make_registry(storage).await;
409 let result = reg.export_snapshot("nonexistent").unwrap();
410 assert!(result.is_none());
411 }
412
413 #[tokio::test]
414 async fn export_versioned_pairs_the_schema_hlc_with_its_snapshot() {
415 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
416 let reg = make_registry(storage).await;
417 let put_hlc = reg.put_schema("v", &simple_schema("v")).await.unwrap();
418
419 let (hlc, snapshot) = reg.export_versioned("v").unwrap().unwrap();
420 assert_eq!(hlc, put_hlc);
421 assert!(!snapshot.is_empty(), "the snapshot carries the schema");
422 assert!(reg.export_versioned("nonexistent").unwrap().is_none());
423 }
424}