reddb_server/storage/unified/store/
impl_file.rs1use 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 self.resident.next_entity_id()
20 }
21
22 pub fn reserve_entity_ids(&self, n: u64) -> std::ops::Range<u64> {
25 self.resident.reserve_entity_ids(n)
26 }
27
28 pub(crate) fn register_entity_id(&self, id: EntityId) {
29 self.resident.register_entity_id(id)
30 }
31
32 pub fn load_from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
44 let file = File::open(path)?;
45 let mut reader = BufReader::new(file);
46 let mut buf = Vec::new();
47 reader.read_to_end(&mut buf)?;
48 Self::load_from_bytes(&buf)
49 }
50
51 pub(crate) fn load_from_bytes(buf: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
52 Self::load_from_bytes_with_config(buf, UnifiedStoreConfig::default())
53 }
54
55 pub(crate) fn load_from_bytes_with_config(
56 buf: &[u8],
57 config: UnifiedStoreConfig,
58 ) -> Result<Self, Box<dyn std::error::Error>> {
59 let (resident, version) =
60 reddb_engine::storage::resident_store::ResidentStore::from_snapshot(
61 buf,
62 &config.manager_config,
63 config.auto_index_refs,
64 config.max_cross_refs,
65 )?;
66 let mut store = Self::with_config(config);
67 store.resident = resident;
68 store.note_result_cache_snapshot_version(version);
69 store.set_format_version(version.max(STORE_VERSION_CURRENT));
70 store.load_result_cache_epoch()?;
71
72 Ok(store)
73 }
74
75 pub fn save_to_file(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
80 let buf = self.to_binary_dump_bytes();
81 reddb_file::write_native_store_bytes_atomically(path, &buf)?;
82 Ok(())
83 }
84
85 pub(crate) fn to_binary_dump_bytes(&self) -> Vec<u8> {
86 let _vault_guard = self.vault_publication_lock.lock();
87 let pending_vault_versions = self.pending_vault_versions.read();
88 let (_, excluded) = self.snapshot_manager.get().map_or_else(
89 || (0, Default::default()),
90 |manager| manager.checkpoint_excluded_state(),
91 );
92 self.resident
93 .snapshot_bytes(&pending_vault_versions, &excluded, || {
94 self.set_format_version(STORE_VERSION_CURRENT);
95 })
96 }
97
98 pub(crate) fn read_entity_binary(
100 buf: &[u8],
101 pos: &mut usize,
102 format_version: u32,
103 ) -> Result<UnifiedEntity, Box<dyn std::error::Error>> {
104 Ok(reddb_engine::storage::entity_codec::read_entity_binary(
105 buf,
106 pos,
107 format_version,
108 )?)
109 }
110
111 pub(crate) fn write_entity_binary(
113 buf: &mut Vec<u8>,
114 entity: &UnifiedEntity,
115 format_version: u32,
116 ) {
117 reddb_engine::storage::entity_codec::write_entity_binary(buf, entity, format_version);
118 }
119}
120
121#[cfg(test)]
122mod aux_metadata_dump_tests {
123 use super::*;
124 use std::collections::HashMap;
125
126 #[test]
127 fn snapshot_load_preserves_existing_wal_and_restores_future_appends() {
128 let directory = tempfile::tempdir().expect("directory");
129 let path = directory.path().join("snapshot.rdb");
130 let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
131 let old =
132 UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text("snapshot")]);
133 store.insert_auto("rows", old).expect("snapshot row");
134 let snapshot = store.to_binary_dump_bytes();
135 crate::storage::EmbeddedRdbArtifact::create_with_snapshot(&path, &snapshot)
136 .expect("artifact");
137 let newer = UnifiedEntity::table_row(
138 EntityId::new(1),
139 "rows",
140 1,
141 vec![Value::text("durable WAL")],
142 );
143 let action = StoreWalAction::upsert_entity("rows", &newer, None, STORE_VERSION_CURRENT);
144 UnifiedStore::with_config(UnifiedStoreConfig::default().with_embedded_wal_path(&path))
145 .finish_paged_write([action])
146 .expect("durable update after snapshot");
147 let before = std::fs::read(&path).expect("original artifact bytes");
148 let loaded = UnifiedStore::load_from_bytes_with_config(
149 &snapshot,
150 UnifiedStoreConfig::default().with_embedded_wal_path(&path),
151 )
152 .expect("reconstruct snapshot");
153 assert_eq!(std::fs::read(&path).expect("artifact after load"), before);
154 assert_eq!(
155 loaded.config().embedded_wal_path.as_deref(),
156 Some(path.as_path())
157 );
158
159 let generation = crate::storage::EmbeddedRdbArtifact::open(&path)
160 .expect("boundary before future write")
161 .selected_superblock
162 .generation;
163 loaded
164 .insert_auto(
165 "rows",
166 UnifiedEntity::table_row(
167 EntityId::new(2),
168 "rows",
169 2,
170 vec![Value::text("future write")],
171 ),
172 )
173 .expect("future writes still append WAL");
174 assert_eq!(
175 crate::storage::EmbeddedRdbArtifact::open(&path)
176 .expect("boundary after future write")
177 .selected_superblock
178 .generation,
179 generation + 1
180 );
181 }
182
183 #[test]
184 fn aux_metadata_round_trips_through_binary_dump() {
185 let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
186 store.create_collection("k").expect("create collection");
187 store.set_aux_metadata(b"contract-blob".to_vec());
188
189 let bytes = store.to_binary_dump_bytes();
190 let reloaded = UnifiedStore::load_from_bytes(&bytes).expect("reload dump");
191
192 assert_eq!(reloaded.aux_metadata(), b"contract-blob".to_vec());
193 }
194
195 #[test]
196 fn empty_aux_metadata_round_trips_as_empty() {
197 let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
198 let bytes = store.to_binary_dump_bytes();
199 let reloaded = UnifiedStore::load_from_bytes(&bytes).expect("reload dump");
200
201 assert!(reloaded.aux_metadata().is_empty());
202 }
203
204 #[test]
205 fn legacy_system_collection_snapshot_loads_and_republishes_canonical_name() {
206 let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
207 store
208 .insert_auto(
209 "red_config",
210 UnifiedEntity::table_row(
211 EntityId::new(1),
212 "red_config",
213 1,
214 vec![Value::text("preserved")],
215 ),
216 )
217 .expect("config row");
218 let mut legacy = store.to_binary_dump_bytes();
219 let canonical = b"red.config";
220 let old = b"red_config";
221 let mut replacements = 0;
222 for offset in 0..=legacy.len() - canonical.len() {
223 if &legacy[offset..offset + canonical.len()] == canonical {
224 legacy[offset..offset + old.len()].copy_from_slice(old);
225 replacements += 1;
226 }
227 }
228 assert!(
229 replacements >= 2,
230 "snapshot carries catalog and entity names"
231 );
232 legacy.truncate(legacy.len() - 4);
233 reddb_file::append_native_store_crc32_footer(&mut legacy);
234
235 let migrated = UnifiedStore::load_from_bytes(&legacy).expect("legacy snapshot");
236 assert_eq!(migrated.list_collections(), vec!["red.config".to_string()]);
237 assert_eq!(
238 migrated
239 .get("red.config", EntityId::new(1))
240 .expect("preserved row")
241 .kind
242 .collection(),
243 "red.config"
244 );
245 let republished = migrated.to_binary_dump_bytes();
246 assert!(
247 republished.windows(old.len()).all(|window| window != old),
248 "new snapshots must not republish legacy identities"
249 );
250 assert!(republished
251 .windows(canonical.len())
252 .any(|window| window == canonical));
253 }
254
255 #[test]
256 fn interned_timeseries_series_reduces_repeated_tag_storage() {
257 let tags = HashMap::from([
258 (
259 "host".to_string(),
260 "srv-very-long-hostname-0001".to_string(),
261 ),
262 ("region".to_string(), "us-east-1".to_string()),
263 ("service".to_string(), "checkout-api".to_string()),
264 ]);
265 let inline_bytes: usize = (0..100)
266 .map(|timestamp_ns| {
267 let entity = UnifiedEntity::new(
268 EntityId::new(0),
269 EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind {
270 series: "cpu_metrics".to_string(),
271 metric: "cpu.idle".to_string(),
272 })),
273 EntityData::TimeSeries(crate::storage::TimeSeriesData {
274 metric: "cpu.idle".to_string(),
275 series_id: None,
276 timestamp_ns,
277 value: 94.8,
278 tags: tags.clone(),
279 fields: HashMap::new(),
280 }),
281 );
282 UnifiedStore::serialize_entity_record(&entity, None, reddb_file::STORE_VERSION_V10)
283 .len()
284 })
285 .sum();
286
287 let mut dictionary_fields = HashMap::new();
288 dictionary_fields.insert("collection".to_string(), Value::text("cpu_metrics"));
289 dictionary_fields.insert("series_id".to_string(), Value::UnsignedInteger(0));
290 dictionary_fields.insert("metric".to_string(), Value::text("cpu.idle"));
291 dictionary_fields.insert(
292 "canonical_tags".to_string(),
293 Value::text(
294 r#"{"host":"srv-very-long-hostname-0001","region":"us-east-1","service":"checkout-api"}"#,
295 ),
296 );
297 dictionary_fields.insert(
298 "tags".to_string(),
299 Value::Json(
300 br#"{"host":"srv-very-long-hostname-0001","region":"us-east-1","service":"checkout-api"}"#
301 .to_vec(),
302 ),
303 );
304 let dictionary = UnifiedEntity::new(
305 EntityId::new(0),
306 EntityKind::TableRow {
307 table: Arc::from("red_timeseries_series"),
308 row_id: 0,
309 },
310 EntityData::Row(RowData {
311 columns: Vec::new(),
312 named: Some(dictionary_fields),
313 schema: None,
314 }),
315 );
316 let dictionary_bytes =
317 UnifiedStore::serialize_entity_record(&dictionary, None, STORE_VERSION_V11).len();
318 let interned_bytes = dictionary_bytes
319 + (0..100)
320 .map(|timestamp_ns| {
321 let entity = UnifiedEntity::new(
322 EntityId::new(0),
323 EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind {
324 series: "cpu_metrics".to_string(),
325 metric: "cpu.idle".to_string(),
326 })),
327 EntityData::TimeSeries(crate::storage::TimeSeriesData {
328 metric: "cpu.idle".to_string(),
329 series_id: Some(0),
330 timestamp_ns,
331 value: 94.8,
332 tags: HashMap::new(),
333 fields: HashMap::new(),
334 }),
335 );
336 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11).len()
337 })
338 .sum::<usize>();
339
340 assert!(
341 interned_bytes < inline_bytes,
342 "interned series storage should shrink repeated tags: inline={inline_bytes}, interned={interned_bytes}"
343 );
344 }
345}