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