1use super::*;
2
3impl UnifiedStore {
4 pub fn new() -> Self {
5 Self::with_config(UnifiedStoreConfig::default())
6 }
7
8 pub fn format_version(&self) -> u32 {
10 self.format_version.load(Ordering::SeqCst)
11 }
12
13 pub(crate) fn set_format_version(&self, version: u32) {
14 self.format_version.store(version, Ordering::SeqCst);
15 }
16
17 pub fn next_entity_id(&self) -> EntityId {
19 EntityId::new(self.next_entity_id.fetch_add(1, Ordering::SeqCst))
20 }
21
22 pub fn reserve_entity_ids(&self, n: u64) -> std::ops::Range<u64> {
25 let start = self.next_entity_id.fetch_add(n, Ordering::SeqCst);
26 start..start + n
27 }
28
29 pub(crate) fn register_entity_id(&self, id: EntityId) {
30 let candidate = id.raw().saturating_add(1);
31 let mut current = self.next_entity_id.load(Ordering::SeqCst);
32 while candidate > current {
33 match self.next_entity_id.compare_exchange(
34 current,
35 candidate,
36 Ordering::SeqCst,
37 Ordering::SeqCst,
38 ) {
39 Ok(_) => break,
40 Err(updated) => current = updated,
41 }
42 }
43 }
44
45 fn restore_snapshot_entity(
48 &self,
49 collection: &str,
50 mut entity: UnifiedEntity,
51 ) -> Result<EntityId, StoreError> {
52 let manager = self.get_or_create_collection(collection);
53 if entity.id.raw() == 0 {
54 entity.id = self.next_entity_id();
55 } else {
56 self.register_entity_id(entity.id);
57 }
58 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
59 if *row_id == 0 {
60 *row_id = manager.next_row_id();
61 } else {
62 manager.register_row_id(*row_id);
63 }
64 }
65 entity.ensure_table_logical_id();
66 self.context_index.index_entity(collection, &entity);
67 if self.config.auto_index_refs {
68 self.index_cross_refs(&entity, collection)?;
69 }
70 let graph_label = match &entity.kind {
71 EntityKind::GraphNode(node) => Some(node.label.clone()),
72 _ => None,
73 };
74 let id = manager.restore_snapshot_entity(entity)?;
75 if let Some(label) = graph_label {
76 self.update_graph_label_index(collection, &label, id);
77 }
78 Ok(id)
79 }
80
81 pub fn load_from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
93 let file = File::open(path)?;
94 let mut reader = BufReader::new(file);
95 let mut buf = Vec::new();
96 reader.read_to_end(&mut buf)?;
97 Self::load_from_bytes(&buf)
98 }
99
100 pub(crate) fn load_from_bytes(buf: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
101 Self::load_from_bytes_with_config(buf, UnifiedStoreConfig::default())
102 }
103
104 pub(crate) fn load_from_bytes_with_config(
105 buf: &[u8],
106 config: UnifiedStoreConfig,
107 ) -> Result<Self, Box<dyn std::error::Error>> {
108 let mut buf = buf.to_vec();
109 let version =
110 reddb_file::decode_native_store_header(&buf).map_err(|err| err.to_string())?;
111 let mut pos = 8;
112
113 reddb_file::verify_native_store_crc32_footer(&mut buf, version)
115 .map_err(|err| err.to_string())?;
116
117 let mut store = Self::with_config(config);
118 let embedded_wal_path = store.config.embedded_wal_path.take();
121 store.set_format_version(version);
122
123 let collection_count =
125 reddb_file::decode_native_dump_count(&buf, &mut pos).map_err(|e| e.to_string())?;
126
127 for _ in 0..collection_count {
129 let collection_header =
130 reddb_file::decode_native_dump_collection_header(&buf, &mut pos)
131 .map_err(|e| e.to_string())?;
132
133 store.get_or_create_collection(&collection_header.name);
137
138 for _ in 0..collection_header.entity_count {
140 if version >= STORE_VERSION_V7 {
141 let record_bytes = reddb_file::decode_native_dump_entity_record(&buf, &mut pos)
143 .map_err(|e| e.to_string())?;
144
145 let (entity, metadata) = Self::deserialize_entity_record(record_bytes, version)
146 .map_err(|e| format!("Entity record deserialization error: {e}"))?;
147
148 store.restore_snapshot_entity(&collection_header.name, entity.clone())?;
149
150 if let Some(metadata) = metadata {
151 if let Some(manager) = store.get_collection(&collection_header.name) {
152 let _ = manager.set_metadata(entity.id, metadata);
153 }
154 }
155 } else {
156 let entity = Self::read_entity_binary(&buf, &mut pos, version)?;
158 store.restore_snapshot_entity(&collection_header.name, entity)?;
159 }
160 }
161 }
162
163 if pos < buf.len() {
164 let cross_ref_count =
166 reddb_file::decode_native_dump_count(&buf, &mut pos).map_err(|e| e.to_string())?;
167
168 for _ in 0..cross_ref_count {
169 let cross_ref = reddb_file::decode_native_dump_cross_ref(&buf, &mut pos)
170 .map_err(|e| e.to_string())?;
171 let ref_type = RefType::from_byte(cross_ref.ref_type);
172
173 let source_collection = store
174 .get_any(EntityId::new(cross_ref.source_id))
175 .map(|(name, _)| name)
176 .unwrap_or_else(|| cross_ref.target_collection.clone());
177 let _ = store.add_cross_ref(
178 &source_collection,
179 EntityId::new(cross_ref.source_id),
180 &cross_ref.target_collection,
181 EntityId::new(cross_ref.target_id),
182 ref_type,
183 1.0,
184 );
185 }
186 }
187
188 if version >= reddb_file::STORE_VERSION_V10 && pos < buf.len() {
193 let aux = reddb_file::decode_native_dump_entity_record(&buf, &mut pos)
194 .map_err(|e| e.to_string())?;
195 if !aux.is_empty() {
196 store.set_aux_metadata(aux.to_vec());
197 }
198 }
199
200 if store.format_version() < STORE_VERSION_CURRENT {
201 store.set_format_version(STORE_VERSION_CURRENT);
202 }
203 store.config.embedded_wal_path = embedded_wal_path;
204
205 Ok(store)
206 }
207
208 pub fn save_to_file(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
213 let buf = self.to_binary_dump_bytes();
214 reddb_file::write_native_store_bytes_atomically(path, &buf)?;
215 Ok(())
216 }
217
218 pub(crate) fn to_binary_dump_bytes(&self) -> Vec<u8> {
219 let _vault_guard = self.vault_publication_lock.lock();
220 let pending_vault_versions = self.pending_vault_versions.read();
221 let (_, excluded) = self.snapshot_manager.get().map_or_else(
222 || (0, Default::default()),
223 |manager| manager.checkpoint_excluded_state(),
224 );
225 let mut buf = Vec::new();
226
227 buf.extend_from_slice(&reddb_file::encode_native_store_header(
230 STORE_VERSION_CURRENT,
231 ));
232
233 let collections = self.collections.read();
235 reddb_file::encode_native_dump_count(&mut buf, collections.len() as u32);
236
237 let fv = STORE_VERSION_CURRENT;
238 for (name, manager) in collections.iter() {
239 let entities = manager.query_all(|entity| {
241 if pending_vault_versions.contains(&entity.id) {
242 return false;
243 }
244 !matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
245 || !excluded.contains(&entity.xmin)
246 });
247 reddb_file::encode_native_dump_collection_header(&mut buf, name, entities.len() as u32);
248
249 for mut entity in entities {
252 if matches!(entity.data, EntityData::Row(_)) && excluded.contains(&entity.xmax) {
255 entity.xmax = 0;
256 }
257 let metadata = manager.get_metadata(entity.id);
258 let record = Self::serialize_entity_record(&entity, metadata.as_ref(), fv);
259 reddb_file::encode_native_dump_entity_record(&mut buf, &record);
260 }
261 }
262
263 let cross_refs = self.cross_refs.read();
265 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
266 reddb_file::encode_native_dump_count(&mut buf, total_refs as u32);
267
268 for (source_id, refs) in cross_refs.iter() {
269 for (target_id, ref_type, collection) in refs {
270 reddb_file::encode_native_dump_cross_ref(
271 &mut buf,
272 source_id.raw(),
273 target_id.raw(),
274 ref_type.to_byte(),
275 collection,
276 );
277 }
278 }
279
280 {
284 let aux = self.aux_metadata.read();
285 reddb_file::encode_native_dump_entity_record(&mut buf, &aux);
286 }
287
288 self.set_format_version(STORE_VERSION_CURRENT);
289
290 reddb_file::append_native_store_crc32_footer(&mut buf);
291 buf
292 }
293
294 pub(crate) fn read_entity_binary(
296 buf: &[u8],
297 pos: &mut usize,
298 format_version: u32,
299 ) -> Result<UnifiedEntity, Box<dyn std::error::Error>> {
300 Ok(reddb_engine::storage::entity_codec::read_entity_binary(
301 buf,
302 pos,
303 format_version,
304 )?)
305 }
306
307 pub(crate) fn write_entity_binary(
309 buf: &mut Vec<u8>,
310 entity: &UnifiedEntity,
311 format_version: u32,
312 ) {
313 reddb_engine::storage::entity_codec::write_entity_binary(buf, entity, format_version);
314 }
315}
316
317#[cfg(test)]
318mod aux_metadata_dump_tests {
319 use super::*;
320 use std::collections::HashMap;
321
322 #[test]
323 fn snapshot_load_preserves_existing_wal_and_restores_future_appends() {
324 let directory = tempfile::tempdir().expect("directory");
325 let path = directory.path().join("snapshot.rdb");
326 let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
327 let old =
328 UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text("snapshot")]);
329 store.insert_auto("rows", old).expect("snapshot row");
330 let snapshot = store.to_binary_dump_bytes();
331 crate::storage::EmbeddedRdbArtifact::create_with_snapshot(&path, &snapshot)
332 .expect("artifact");
333 let newer = UnifiedEntity::table_row(
334 EntityId::new(1),
335 "rows",
336 1,
337 vec![Value::text("durable WAL")],
338 );
339 let action = StoreWalAction::upsert_entity("rows", &newer, None, STORE_VERSION_CURRENT);
340 UnifiedStore::with_config(UnifiedStoreConfig::default().with_embedded_wal_path(&path))
341 .finish_paged_write([action])
342 .expect("durable update after snapshot");
343 let before = std::fs::read(&path).expect("original artifact bytes");
344 let loaded = UnifiedStore::load_from_bytes_with_config(
345 &snapshot,
346 UnifiedStoreConfig::default().with_embedded_wal_path(&path),
347 )
348 .expect("reconstruct snapshot");
349 assert_eq!(std::fs::read(&path).expect("artifact after load"), before);
350 assert_eq!(
351 loaded.config().embedded_wal_path.as_deref(),
352 Some(path.as_path())
353 );
354
355 let generation = crate::storage::EmbeddedRdbArtifact::open(&path)
356 .expect("boundary before future write")
357 .selected_superblock
358 .generation;
359 loaded
360 .insert_auto(
361 "rows",
362 UnifiedEntity::table_row(
363 EntityId::new(2),
364 "rows",
365 2,
366 vec![Value::text("future write")],
367 ),
368 )
369 .expect("future writes still append WAL");
370 assert_eq!(
371 crate::storage::EmbeddedRdbArtifact::open(&path)
372 .expect("boundary after future write")
373 .selected_superblock
374 .generation,
375 generation + 1
376 );
377 }
378
379 #[test]
380 fn aux_metadata_round_trips_through_binary_dump() {
381 let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
382 store.create_collection("k").expect("create collection");
383 store.set_aux_metadata(b"contract-blob".to_vec());
384
385 let bytes = store.to_binary_dump_bytes();
386 let reloaded = UnifiedStore::load_from_bytes(&bytes).expect("reload dump");
387
388 assert_eq!(reloaded.aux_metadata(), b"contract-blob".to_vec());
389 }
390
391 #[test]
392 fn empty_aux_metadata_round_trips_as_empty() {
393 let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
394 let bytes = store.to_binary_dump_bytes();
395 let reloaded = UnifiedStore::load_from_bytes(&bytes).expect("reload dump");
396
397 assert!(reloaded.aux_metadata().is_empty());
398 }
399
400 #[test]
401 fn interned_timeseries_series_reduces_repeated_tag_storage() {
402 let tags = HashMap::from([
403 (
404 "host".to_string(),
405 "srv-very-long-hostname-0001".to_string(),
406 ),
407 ("region".to_string(), "us-east-1".to_string()),
408 ("service".to_string(), "checkout-api".to_string()),
409 ]);
410 let inline_bytes: usize = (0..100)
411 .map(|timestamp_ns| {
412 let entity = UnifiedEntity::new(
413 EntityId::new(0),
414 EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind {
415 series: "cpu_metrics".to_string(),
416 metric: "cpu.idle".to_string(),
417 })),
418 EntityData::TimeSeries(crate::storage::TimeSeriesData {
419 metric: "cpu.idle".to_string(),
420 series_id: None,
421 timestamp_ns,
422 value: 94.8,
423 tags: tags.clone(),
424 fields: HashMap::new(),
425 }),
426 );
427 UnifiedStore::serialize_entity_record(&entity, None, reddb_file::STORE_VERSION_V10)
428 .len()
429 })
430 .sum();
431
432 let mut dictionary_fields = HashMap::new();
433 dictionary_fields.insert("collection".to_string(), Value::text("cpu_metrics"));
434 dictionary_fields.insert("series_id".to_string(), Value::UnsignedInteger(0));
435 dictionary_fields.insert("metric".to_string(), Value::text("cpu.idle"));
436 dictionary_fields.insert(
437 "canonical_tags".to_string(),
438 Value::text(
439 r#"{"host":"srv-very-long-hostname-0001","region":"us-east-1","service":"checkout-api"}"#,
440 ),
441 );
442 dictionary_fields.insert(
443 "tags".to_string(),
444 Value::Json(
445 br#"{"host":"srv-very-long-hostname-0001","region":"us-east-1","service":"checkout-api"}"#
446 .to_vec(),
447 ),
448 );
449 let dictionary = UnifiedEntity::new(
450 EntityId::new(0),
451 EntityKind::TableRow {
452 table: Arc::from("red_timeseries_series"),
453 row_id: 0,
454 },
455 EntityData::Row(RowData {
456 columns: Vec::new(),
457 named: Some(dictionary_fields),
458 schema: None,
459 }),
460 );
461 let dictionary_bytes =
462 UnifiedStore::serialize_entity_record(&dictionary, None, STORE_VERSION_V11).len();
463 let interned_bytes = dictionary_bytes
464 + (0..100)
465 .map(|timestamp_ns| {
466 let entity = UnifiedEntity::new(
467 EntityId::new(0),
468 EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind {
469 series: "cpu_metrics".to_string(),
470 metric: "cpu.idle".to_string(),
471 })),
472 EntityData::TimeSeries(crate::storage::TimeSeriesData {
473 metric: "cpu.idle".to_string(),
474 series_id: Some(0),
475 timestamp_ns,
476 value: 94.8,
477 tags: HashMap::new(),
478 fields: HashMap::new(),
479 }),
480 );
481 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11).len()
482 })
483 .sum::<usize>();
484
485 assert!(
486 interned_bytes < inline_bytes,
487 "interned series storage should shrink repeated tags: inline={inline_bytes}, interned={interned_bytes}"
488 );
489 }
490}