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 interned_timeseries_series_reduces_repeated_tag_storage() {
204 let tags = HashMap::from([
205 (
206 "host".to_string(),
207 "srv-very-long-hostname-0001".to_string(),
208 ),
209 ("region".to_string(), "us-east-1".to_string()),
210 ("service".to_string(), "checkout-api".to_string()),
211 ]);
212 let inline_bytes: usize = (0..100)
213 .map(|timestamp_ns| {
214 let entity = UnifiedEntity::new(
215 EntityId::new(0),
216 EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind {
217 series: "cpu_metrics".to_string(),
218 metric: "cpu.idle".to_string(),
219 })),
220 EntityData::TimeSeries(crate::storage::TimeSeriesData {
221 metric: "cpu.idle".to_string(),
222 series_id: None,
223 timestamp_ns,
224 value: 94.8,
225 tags: tags.clone(),
226 fields: HashMap::new(),
227 }),
228 );
229 UnifiedStore::serialize_entity_record(&entity, None, reddb_file::STORE_VERSION_V10)
230 .len()
231 })
232 .sum();
233
234 let mut dictionary_fields = HashMap::new();
235 dictionary_fields.insert("collection".to_string(), Value::text("cpu_metrics"));
236 dictionary_fields.insert("series_id".to_string(), Value::UnsignedInteger(0));
237 dictionary_fields.insert("metric".to_string(), Value::text("cpu.idle"));
238 dictionary_fields.insert(
239 "canonical_tags".to_string(),
240 Value::text(
241 r#"{"host":"srv-very-long-hostname-0001","region":"us-east-1","service":"checkout-api"}"#,
242 ),
243 );
244 dictionary_fields.insert(
245 "tags".to_string(),
246 Value::Json(
247 br#"{"host":"srv-very-long-hostname-0001","region":"us-east-1","service":"checkout-api"}"#
248 .to_vec(),
249 ),
250 );
251 let dictionary = UnifiedEntity::new(
252 EntityId::new(0),
253 EntityKind::TableRow {
254 table: Arc::from("red_timeseries_series"),
255 row_id: 0,
256 },
257 EntityData::Row(RowData {
258 columns: Vec::new(),
259 named: Some(dictionary_fields),
260 schema: None,
261 }),
262 );
263 let dictionary_bytes =
264 UnifiedStore::serialize_entity_record(&dictionary, None, STORE_VERSION_V11).len();
265 let interned_bytes = dictionary_bytes
266 + (0..100)
267 .map(|timestamp_ns| {
268 let entity = UnifiedEntity::new(
269 EntityId::new(0),
270 EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind {
271 series: "cpu_metrics".to_string(),
272 metric: "cpu.idle".to_string(),
273 })),
274 EntityData::TimeSeries(crate::storage::TimeSeriesData {
275 metric: "cpu.idle".to_string(),
276 series_id: Some(0),
277 timestamp_ns,
278 value: 94.8,
279 tags: HashMap::new(),
280 fields: HashMap::new(),
281 }),
282 );
283 UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11).len()
284 })
285 .sum::<usize>();
286
287 assert!(
288 interned_bytes < inline_bytes,
289 "interned series storage should shrink repeated tags: inline={inline_bytes}, interned={interned_bytes}"
290 );
291 }
292}