Skip to main content

reddb_server/storage/unified/store/
impl_file.rs

1use super::*;
2
3impl UnifiedStore {
4    pub fn new() -> Self {
5        Self::with_config(UnifiedStoreConfig::default())
6    }
7
8    /// Get the current storage format version
9    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    /// Allocate a global entity ID
18    pub fn next_entity_id(&self) -> EntityId {
19        EntityId::new(self.next_entity_id.fetch_add(1, Ordering::SeqCst))
20    }
21
22    /// Reserve `n` contiguous global entity IDs with one fetch_add.
23    /// Caller assigns `id = EntityId::new(start + i)` per entity.
24    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    /// Load a persisted image and rebuild its derived indexes, retaining the
46    /// stored sequence instead of assigning one from snapshot iteration order.
47    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    /// Load store from binary file
82    ///
83    /// Binary dump framing is defined by `reddb_file::native_store`.
84    ///
85    /// Store payload shape:
86    /// ```text
87    /// [collection_count: varu32]
88    /// [collections...]
89    /// [cross_ref_count: varu32]
90    /// [cross_refs...]
91    /// ```
92    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        // V3+ has CRC32 footer — verify integrity before parsing
114        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        // Reconstruct the snapshot without appending its old images to the
119        // live WAL. The caller replays that WAL after loading this snapshot.
120        let embedded_wal_path = store.config.embedded_wal_path.take();
121        store.set_format_version(version);
122
123        // Read collection count
124        let collection_count =
125            reddb_file::decode_native_dump_count(&buf, &mut pos).map_err(|e| e.to_string())?;
126
127        // Read each collection
128        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            // The registry is persisted even when the last entity was removed.
134            // Restore it independently of the entity loop so empty queues and
135            // other collections survive reopening without emitting new WAL.
136            store.get_or_create_collection(&collection_header.name);
137
138            // Read each entity — V7+ includes metadata alongside entity data.
139            for _ in 0..collection_header.entity_count {
140                if version >= STORE_VERSION_V7 {
141                    // Length-prefixed entity+metadata record (serialize_entity_record format)
142                    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                    // V1–V6: entity only, no metadata
157                    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            // Read cross-references section
165            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        // V10+: a trailing opaque auxiliary-metadata blob follows the
189        // cross-references. RedDB stores the collection contracts here so the
190        // single-file artifact carries each collection's declared model across
191        // a restart. Older dumps end at the cross-refs and have no blob.
192        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    /// Save store to binary file
209    ///
210    /// Uses compact binary encoding with varint for efficient storage.
211    /// No JSON - pure binary with pages and indices.
212    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        // Version 9 includes explicit table-row logical identity plus MVCC
228        // xmin/xmax alongside the V7 metadata envelope.
229        buf.extend_from_slice(&reddb_file::encode_native_store_header(
230            STORE_VERSION_CURRENT,
231        ));
232
233        // Get all collections
234        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            // Get all entities from this collection
240            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            // V7+: serialize entity+metadata as a length-prefixed record.
250            // Each record: [u32 len][serialize_entity_record bytes]
251            for mut entity in entities {
252                // Unfinished and rolled-back deletions cannot become committed
253                // tombstones when recovery reconstructs the xid floor.
254                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        // Write cross-references
264        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        // V10+: trailing opaque auxiliary-metadata blob (collection contracts
281        // for the single-file artifact). Always written — empty when unused —
282        // so the read side can unconditionally expect it for V10 dumps.
283        {
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    /// Read entity from binary buffer
295    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        // Entity ID
301        let id = read_varu64(buf, pos).map_err(|e| format!("Failed to read entity id: {:?}", e))?;
302
303        // EntityKind type byte
304        let kind_type = buf[*pos];
305        *pos += 1;
306
307        // EntityKind details
308        let kind = match kind_type {
309            0 => {
310                // TableRow
311                let table_len = Self::read_varu32_safe(buf, pos)?;
312                let table = String::from_utf8(buf[*pos..*pos + table_len].to_vec())?;
313                *pos += table_len;
314                let row_id = Self::read_varu64_safe(buf, pos)?;
315                EntityKind::TableRow {
316                    table: table.into(),
317                    row_id,
318                }
319            }
320            1 => {
321                // GraphNode
322                let label_len = Self::read_varu32_safe(buf, pos)?;
323                let label = String::from_utf8(buf[*pos..*pos + label_len].to_vec())?;
324                *pos += label_len;
325                let node_type_len = Self::read_varu32_safe(buf, pos)?;
326                let node_type = String::from_utf8(buf[*pos..*pos + node_type_len].to_vec())?;
327                *pos += node_type_len;
328                EntityKind::GraphNode(Box::new(GraphNodeKind { label, node_type }))
329            }
330            2 => {
331                // GraphEdge
332                let label_len = Self::read_varu32_safe(buf, pos)?;
333                let label = String::from_utf8(buf[*pos..*pos + label_len].to_vec())?;
334                *pos += label_len;
335                let from_node_len = Self::read_varu32_safe(buf, pos)?;
336                let from_node = String::from_utf8(buf[*pos..*pos + from_node_len].to_vec())?;
337                *pos += from_node_len;
338                let to_node_len = Self::read_varu32_safe(buf, pos)?;
339                let to_node = String::from_utf8(buf[*pos..*pos + to_node_len].to_vec())?;
340                *pos += to_node_len;
341                let weight =
342                    u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
343                *pos += 4;
344                EntityKind::GraphEdge(Box::new(GraphEdgeKind {
345                    label,
346                    from_node,
347                    to_node,
348                    weight,
349                }))
350            }
351            3 => {
352                // Vector
353                let collection_len = Self::read_varu32_safe(buf, pos)?;
354                let collection = String::from_utf8(buf[*pos..*pos + collection_len].to_vec())?;
355                *pos += collection_len;
356                EntityKind::Vector { collection }
357            }
358            4 => {
359                // TimeSeriesPoint
360                let series_len = Self::read_varu32_safe(buf, pos)?;
361                let series = String::from_utf8(buf[*pos..*pos + series_len].to_vec())?;
362                *pos += series_len;
363                let metric_len = Self::read_varu32_safe(buf, pos)?;
364                let metric = String::from_utf8(buf[*pos..*pos + metric_len].to_vec())?;
365                *pos += metric_len;
366                EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind { series, metric }))
367            }
368            5 => {
369                // QueueMessage
370                let queue_len = Self::read_varu32_safe(buf, pos)?;
371                let queue = String::from_utf8(buf[*pos..*pos + queue_len].to_vec())?;
372                *pos += queue_len;
373                let position = Self::read_varu64_safe(buf, pos)?;
374                EntityKind::QueueMessage { queue, position }
375            }
376            _ => return Err(format!("Unknown EntityKind type: {}", kind_type).into()),
377        };
378
379        // EntityData type byte
380        let data_type = buf[*pos];
381        *pos += 1;
382
383        // EntityData
384        let mut data = match data_type {
385            0 => {
386                // Row
387                let col_count = Self::read_varu32_safe(buf, pos)?;
388                let mut columns = Vec::with_capacity(col_count);
389                for _ in 0..col_count {
390                    columns.push(Self::read_value_binary(buf, pos)?);
391                }
392                EntityData::Row(RowData::new(columns))
393            }
394            1 => {
395                // Node
396                let prop_count = Self::read_varu32_safe(buf, pos)?;
397                let mut properties = HashMap::new();
398                for _ in 0..prop_count {
399                    let key_len = Self::read_varu32_safe(buf, pos)?;
400                    let key = String::from_utf8(buf[*pos..*pos + key_len].to_vec())?;
401                    *pos += key_len;
402                    let value = Self::read_value_binary(buf, pos)?;
403                    properties.insert(key, value);
404                }
405                EntityData::Node(NodeData::with_properties(properties))
406            }
407            2 => {
408                // Edge
409                let weight_bytes = [buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]];
410                *pos += 4;
411                let weight = f32::from_le_bytes(weight_bytes);
412                let prop_count = Self::read_varu32_safe(buf, pos)?;
413                let mut properties = HashMap::new();
414                for _ in 0..prop_count {
415                    let key_len = Self::read_varu32_safe(buf, pos)?;
416                    let key = String::from_utf8(buf[*pos..*pos + key_len].to_vec())?;
417                    *pos += key_len;
418                    let value = Self::read_value_binary(buf, pos)?;
419                    properties.insert(key, value);
420                }
421                let mut edge = EdgeData::new(weight);
422                edge.properties = properties;
423                EntityData::Edge(edge)
424            }
425            3 => {
426                // Vector
427                let dim = Self::read_varu32_safe(buf, pos)?;
428                let mut dense = Vec::with_capacity(dim);
429                for _ in 0..dim {
430                    let bytes = [buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]];
431                    *pos += 4;
432                    dense.push(f32::from_le_bytes(bytes));
433                }
434                let mut vector = VectorData::new(dense);
435                if format_version >= STORE_VERSION_V6 {
436                    let has_content = buf[*pos] != 0;
437                    *pos += 1;
438                    if has_content {
439                        let content_len = Self::read_varu32_safe(buf, pos)?;
440                        vector.content =
441                            Some(String::from_utf8(buf[*pos..*pos + content_len].to_vec())?);
442                        *pos += content_len;
443                    }
444                }
445                EntityData::Vector(vector)
446            }
447            4 => {
448                // TimeSeries
449                let metric_len = Self::read_varu32_safe(buf, pos)?;
450                let metric = String::from_utf8(buf[*pos..*pos + metric_len].to_vec())?;
451                *pos += metric_len;
452                let timestamp_ns = Self::read_varu64_safe(buf, pos)?;
453                let value_bytes = [
454                    buf[*pos],
455                    buf[*pos + 1],
456                    buf[*pos + 2],
457                    buf[*pos + 3],
458                    buf[*pos + 4],
459                    buf[*pos + 5],
460                    buf[*pos + 6],
461                    buf[*pos + 7],
462                ];
463                *pos += 8;
464                let mut tags = HashMap::new();
465                let series_id = if format_version >= STORE_VERSION_V11 {
466                    // Discriminator: interned series id (1) vs inline tag map (0).
467                    // Points interned through the series dictionary carry a series
468                    // id and no inline tags; paths that do not intern (metrics
469                    // remote-write, rollups) keep their tags inline.
470                    let interned = Self::read_varu32_safe(buf, pos)?;
471                    if interned == 1 {
472                        Some(Self::read_varu64_safe(buf, pos)?)
473                    } else {
474                        let tag_count = Self::read_varu32_safe(buf, pos)?;
475                        tags = HashMap::with_capacity(tag_count);
476                        for _ in 0..tag_count {
477                            let key_len = Self::read_varu32_safe(buf, pos)?;
478                            let key = String::from_utf8(buf[*pos..*pos + key_len].to_vec())?;
479                            *pos += key_len;
480                            let value_len = Self::read_varu32_safe(buf, pos)?;
481                            let value = String::from_utf8(buf[*pos..*pos + value_len].to_vec())?;
482                            *pos += value_len;
483                            tags.insert(key, value);
484                        }
485                        None
486                    }
487                } else if format_version >= STORE_VERSION_V5 {
488                    let tag_count = Self::read_varu32_safe(buf, pos)?;
489                    tags = HashMap::with_capacity(tag_count);
490                    for _ in 0..tag_count {
491                        let key_len = Self::read_varu32_safe(buf, pos)?;
492                        let key = String::from_utf8(buf[*pos..*pos + key_len].to_vec())?;
493                        *pos += key_len;
494                        let value_len = Self::read_varu32_safe(buf, pos)?;
495                        let value = String::from_utf8(buf[*pos..*pos + value_len].to_vec())?;
496                        *pos += value_len;
497                        tags.insert(key, value);
498                    }
499                    None
500                } else {
501                    None
502                };
503                let fields = if format_version >= STORE_VERSION_V12 {
504                    let field_count = Self::read_varu32_safe(buf, pos)?;
505                    let mut fields = HashMap::with_capacity(field_count);
506                    for _ in 0..field_count {
507                        let key_len = Self::read_varu32_safe(buf, pos)?;
508                        let key = String::from_utf8(buf[*pos..*pos + key_len].to_vec())?;
509                        *pos += key_len;
510                        let value = Self::read_value_binary(buf, pos)?;
511                        fields.insert(key, value);
512                    }
513                    fields
514                } else {
515                    HashMap::new()
516                };
517                EntityData::TimeSeries(crate::storage::unified::entity::TimeSeriesData {
518                    metric,
519                    series_id,
520                    timestamp_ns,
521                    value: f64::from_le_bytes(value_bytes),
522                    tags,
523                    fields,
524                })
525            }
526            5 => {
527                // QueueMessage
528                let payload = Self::read_value_binary(buf, pos)?;
529                let enqueued_at_ns = Self::read_varu64_safe(buf, pos)?;
530                let attempts = Self::read_varu32_safe(buf, pos)? as u32;
531                EntityData::QueueMessage(crate::storage::unified::entity::QueueMessageData {
532                    payload,
533                    priority: None,
534                    enqueued_at_ns,
535                    attempts,
536                    max_attempts: 3,
537                    acked: false,
538                })
539            }
540            6 => {
541                // Row with named HashMap
542                let field_count = Self::read_varu32_safe(buf, pos)?;
543                let mut named = HashMap::with_capacity(field_count);
544                for _ in 0..field_count {
545                    let key_len = Self::read_varu32_safe(buf, pos)?;
546                    let key = String::from_utf8(buf[*pos..*pos + key_len].to_vec())?;
547                    *pos += key_len;
548                    let value = Self::read_value_binary(buf, pos)?;
549                    named.insert(key, value);
550                }
551                EntityData::Row(RowData {
552                    columns: Vec::new(),
553                    named: Some(named),
554                    schema: None,
555                })
556            }
557            _ => return Err(format!("Unknown EntityData type: {}", data_type).into()),
558        };
559
560        // Timestamps
561        let created_at = Self::read_varu64_safe(buf, pos)?;
562        let updated_at = Self::read_varu64_safe(buf, pos)?;
563
564        // Embeddings count
565        let embedding_count = Self::read_varu32_safe(buf, pos)?;
566        let mut embeddings = Vec::with_capacity(embedding_count);
567        for _ in 0..embedding_count {
568            let name_len = Self::read_varu32_safe(buf, pos)?;
569            let name = String::from_utf8(buf[*pos..*pos + name_len].to_vec())?;
570            *pos += name_len;
571
572            let dim = Self::read_varu32_safe(buf, pos)?;
573            let mut vector = Vec::with_capacity(dim);
574            for _ in 0..dim {
575                let bytes = [buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]];
576                *pos += 4;
577                vector.push(f32::from_le_bytes(bytes));
578            }
579
580            let model_len = Self::read_varu32_safe(buf, pos)?;
581            let model = String::from_utf8(buf[*pos..*pos + model_len].to_vec())?;
582            *pos += model_len;
583
584            embeddings.push(EmbeddingSlot::new(name, vector, model));
585        }
586
587        // Cross-refs count
588        let cross_ref_count = Self::read_varu32_safe(buf, pos)?;
589        let mut cross_refs = Vec::with_capacity(cross_ref_count);
590        for _ in 0..cross_ref_count {
591            let source = Self::read_varu64_safe(buf, pos)?;
592            let target = Self::read_varu64_safe(buf, pos)?;
593            let ref_type_byte = buf[*pos];
594            *pos += 1;
595            let (target_collection, weight, created_at) = if format_version >= STORE_VERSION_V2 {
596                let coll_len = Self::read_varu32_safe(buf, pos)?;
597                let collection = String::from_utf8(buf[*pos..*pos + coll_len].to_vec())?;
598                *pos += coll_len;
599                let weight_bytes = [buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]];
600                *pos += 4;
601                let weight = f32::from_le_bytes(weight_bytes);
602                let created_at = Self::read_varu64_safe(buf, pos)?;
603                (collection, weight, created_at)
604            } else {
605                (String::new(), 1.0, 0)
606            };
607
608            let mut cross_ref = CrossRef::new(
609                EntityId::new(source),
610                EntityId::new(target),
611                target_collection,
612                RefType::from_byte(ref_type_byte),
613            );
614            cross_ref.weight = weight;
615            cross_ref.created_at = created_at;
616            cross_refs.push(cross_ref);
617        }
618
619        // Sequence ID
620        let sequence_id = Self::read_varu64_safe(buf, pos)?;
621
622        if format_version >= STORE_VERSION_V4 {
623            if let EntityData::QueueMessage(message) = &mut data {
624                if *pos < buf.len() {
625                    let priority_present = buf[*pos] != 0;
626                    *pos += 1;
627                    message.priority = if priority_present {
628                        if *pos + 4 > buf.len() {
629                            return Err("truncated queue priority".into());
630                        }
631                        let bytes = [buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]];
632                        *pos += 4;
633                        Some(i32::from_le_bytes(bytes))
634                    } else {
635                        None
636                    };
637                    message.max_attempts = Self::read_varu32_safe(buf, pos)? as u32;
638                    if *pos >= buf.len() {
639                        return Err("truncated queue ack flag".into());
640                    }
641                    message.acked = buf[*pos] != 0;
642                    *pos += 1;
643                }
644            }
645        }
646
647        let mut entity = UnifiedEntity::new(EntityId::new(id), kind, data);
648        entity.created_at = created_at;
649        entity.updated_at = updated_at;
650        entity.sequence_id = sequence_id;
651        if format_version >= STORE_VERSION_V8 && *pos < buf.len() {
652            let has_logical_id = buf[*pos] != 0;
653            *pos += 1;
654            if has_logical_id {
655                let logical_id = Self::read_varu64_safe(buf, pos)?;
656                entity.set_logical_id(EntityId::new(logical_id));
657            }
658        }
659        if format_version >= STORE_VERSION_V9 && *pos < buf.len() {
660            let xmin = Self::read_varu64_safe(buf, pos)?;
661            let xmax = Self::read_varu64_safe(buf, pos)?;
662            entity.set_xmin(xmin);
663            entity.set_xmax(xmax);
664        }
665        if !embeddings.is_empty() || !cross_refs.is_empty() {
666            entity.embeddings_mut().extend(embeddings);
667            entity.cross_refs_mut().extend(cross_refs);
668        }
669
670        Ok(entity)
671    }
672
673    /// Safe varu32 reader that converts DecodeError to Box<dyn Error>
674    fn read_varu32_safe(buf: &[u8], pos: &mut usize) -> Result<usize, Box<dyn std::error::Error>> {
675        read_varu32(buf, pos)
676            .map(|v| v as usize)
677            .map_err(|e| format!("Decode error: {:?}", e).into())
678    }
679
680    /// Safe varu64 reader that converts DecodeError to Box<dyn Error>
681    fn read_varu64_safe(buf: &[u8], pos: &mut usize) -> Result<u64, Box<dyn std::error::Error>> {
682        read_varu64(buf, pos).map_err(|e| format!("Decode error: {:?}", e).into())
683    }
684
685    /// Write entity to binary buffer
686    pub(crate) fn write_entity_binary(
687        buf: &mut Vec<u8>,
688        entity: &UnifiedEntity,
689        format_version: u32,
690    ) {
691        // Entity ID
692        write_varu64(buf, entity.id.raw());
693
694        // EntityKind
695        match &entity.kind {
696            EntityKind::TableRow { table, row_id } => {
697                buf.push(0);
698                write_varu32(buf, table.len() as u32);
699                buf.extend_from_slice(table.as_bytes());
700                write_varu64(buf, *row_id);
701            }
702            EntityKind::GraphNode(ref node) => {
703                buf.push(1);
704                write_varu32(buf, node.label.len() as u32);
705                buf.extend_from_slice(node.label.as_bytes());
706                write_varu32(buf, node.node_type.len() as u32);
707                buf.extend_from_slice(node.node_type.as_bytes());
708            }
709            EntityKind::GraphEdge(ref edge) => {
710                buf.push(2);
711                write_varu32(buf, edge.label.len() as u32);
712                buf.extend_from_slice(edge.label.as_bytes());
713                write_varu32(buf, edge.from_node.len() as u32);
714                buf.extend_from_slice(edge.from_node.as_bytes());
715                write_varu32(buf, edge.to_node.len() as u32);
716                buf.extend_from_slice(edge.to_node.as_bytes());
717                buf.extend_from_slice(&edge.weight.to_le_bytes());
718            }
719            EntityKind::Vector { collection } => {
720                buf.push(3);
721                write_varu32(buf, collection.len() as u32);
722                buf.extend_from_slice(collection.as_bytes());
723            }
724            EntityKind::TimeSeriesPoint(ref ts) => {
725                buf.push(4);
726                write_varu32(buf, ts.series.len() as u32);
727                buf.extend_from_slice(ts.series.as_bytes());
728                write_varu32(buf, ts.metric.len() as u32);
729                buf.extend_from_slice(ts.metric.as_bytes());
730            }
731            EntityKind::QueueMessage { queue, position } => {
732                buf.push(5);
733                write_varu32(buf, queue.len() as u32);
734                buf.extend_from_slice(queue.as_bytes());
735                write_varu64(buf, *position);
736            }
737        }
738
739        // EntityData
740        match &entity.data {
741            EntityData::Row(row) => {
742                if let Some(ref named) = row.named {
743                    // Named row: type 6 = Row with named HashMap.
744                    // Sort by key so the on-disk byte sequence is
745                    // deterministic — replication relies on hashing the
746                    // serialized record to detect divergence, and a
747                    // HashMap-iteration-order race produces spurious
748                    // mismatches.
749                    buf.push(6);
750                    write_varu32(buf, named.len() as u32);
751                    let mut entries: Vec<_> = named.iter().collect();
752                    entries.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
753                    for (key, value) in entries {
754                        write_varu32(buf, key.len() as u32);
755                        buf.extend_from_slice(key.as_bytes());
756                        Self::write_value_binary(buf, value);
757                    }
758                } else {
759                    buf.push(0);
760                    write_varu32(buf, row.columns.len() as u32);
761                    for col in &row.columns {
762                        Self::write_value_binary(buf, col);
763                    }
764                }
765            }
766            EntityData::Node(node) => {
767                buf.push(1);
768                write_varu32(buf, node.properties.len() as u32);
769                let mut entries: Vec<_> = node.properties.iter().collect();
770                entries.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
771                for (key, value) in entries {
772                    write_varu32(buf, key.len() as u32);
773                    buf.extend_from_slice(key.as_bytes());
774                    Self::write_value_binary(buf, value);
775                }
776            }
777            EntityData::Edge(edge) => {
778                buf.push(2);
779                buf.extend_from_slice(&edge.weight.to_le_bytes());
780                write_varu32(buf, edge.properties.len() as u32);
781                let mut entries: Vec<_> = edge.properties.iter().collect();
782                entries.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
783                for (key, value) in entries {
784                    write_varu32(buf, key.len() as u32);
785                    buf.extend_from_slice(key.as_bytes());
786                    Self::write_value_binary(buf, value);
787                }
788            }
789            EntityData::Vector(vec) => {
790                buf.push(3);
791                write_varu32(buf, vec.dense.len() as u32);
792                for f in &vec.dense {
793                    buf.extend_from_slice(&f.to_le_bytes());
794                }
795                if format_version >= STORE_VERSION_V6 {
796                    buf.push(u8::from(vec.content.is_some()));
797                    if let Some(content) = &vec.content {
798                        write_varu32(buf, content.len() as u32);
799                        buf.extend_from_slice(content.as_bytes());
800                    }
801                }
802            }
803            EntityData::TimeSeries(ts) => {
804                buf.push(4);
805                write_varu32(buf, ts.metric.len() as u32);
806                buf.extend_from_slice(ts.metric.as_bytes());
807                write_varu64(buf, ts.timestamp_ns);
808                buf.extend_from_slice(&ts.value.to_le_bytes());
809                if format_version >= STORE_VERSION_V11 {
810                    // Discriminator: interned series id (1) vs inline tag map (0).
811                    // Interned points (dictionary path) persist just the id;
812                    // un-interned points (metrics remote-write, rollups) keep
813                    // their tags inline so the metrics read paths, which build
814                    // labels directly from `tags`, survive a reopen.
815                    match ts.series_id {
816                        Some(series_id) => {
817                            write_varu32(buf, 1);
818                            write_varu64(buf, series_id);
819                        }
820                        None => {
821                            write_varu32(buf, 0);
822                            write_varu32(buf, ts.tags.len() as u32);
823                            let mut tag_entries: Vec<_> = ts.tags.iter().collect();
824                            tag_entries.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
825                            for (key, value) in tag_entries {
826                                write_varu32(buf, key.len() as u32);
827                                buf.extend_from_slice(key.as_bytes());
828                                write_varu32(buf, value.len() as u32);
829                                buf.extend_from_slice(value.as_bytes());
830                            }
831                        }
832                    }
833                } else if format_version >= STORE_VERSION_V5 {
834                    write_varu32(buf, ts.tags.len() as u32);
835                    let mut tag_entries: Vec<_> = ts.tags.iter().collect();
836                    tag_entries.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
837                    for (key, value) in tag_entries {
838                        write_varu32(buf, key.len() as u32);
839                        buf.extend_from_slice(key.as_bytes());
840                        write_varu32(buf, value.len() as u32);
841                        buf.extend_from_slice(value.as_bytes());
842                    }
843                }
844                if format_version >= STORE_VERSION_V12 {
845                    write_varu32(buf, ts.fields.len() as u32);
846                    let mut field_entries: Vec<_> = ts.fields.iter().collect();
847                    field_entries.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
848                    for (key, value) in field_entries {
849                        write_varu32(buf, key.len() as u32);
850                        buf.extend_from_slice(key.as_bytes());
851                        Self::write_value_binary(buf, value);
852                    }
853                }
854            }
855            EntityData::QueueMessage(msg) => {
856                buf.push(5);
857                Self::write_value_binary(buf, &msg.payload);
858                write_varu64(buf, msg.enqueued_at_ns);
859                write_varu32(buf, msg.attempts);
860            }
861        }
862
863        // Timestamps
864        write_varu64(buf, entity.created_at);
865        write_varu64(buf, entity.updated_at);
866
867        // Embeddings
868        write_varu32(buf, entity.embeddings().len() as u32);
869        for emb in entity.embeddings() {
870            write_varu32(buf, emb.name.len() as u32);
871            buf.extend_from_slice(emb.name.as_bytes());
872            write_varu32(buf, emb.vector.len() as u32);
873            for f in &emb.vector {
874                buf.extend_from_slice(&f.to_le_bytes());
875            }
876            write_varu32(buf, emb.model.len() as u32);
877            buf.extend_from_slice(emb.model.as_bytes());
878        }
879
880        // Cross-refs
881        write_varu32(buf, entity.cross_refs().len() as u32);
882        for cross_ref in entity.cross_refs() {
883            write_varu64(buf, cross_ref.source.raw());
884            write_varu64(buf, cross_ref.target.raw());
885            buf.push(cross_ref.ref_type.to_byte());
886            if format_version >= STORE_VERSION_V2 {
887                write_varu32(buf, cross_ref.target_collection.len() as u32);
888                buf.extend_from_slice(cross_ref.target_collection.as_bytes());
889                buf.extend_from_slice(&cross_ref.weight.to_le_bytes());
890                write_varu64(buf, cross_ref.created_at);
891            }
892        }
893
894        // Sequence ID
895        write_varu64(buf, entity.sequence_id);
896
897        if format_version >= STORE_VERSION_V4 {
898            if let EntityData::QueueMessage(message) = &entity.data {
899                buf.push(u8::from(message.priority.is_some()));
900                if let Some(priority) = message.priority {
901                    buf.extend_from_slice(&priority.to_le_bytes());
902                }
903                write_varu32(buf, message.max_attempts);
904                buf.push(u8::from(message.acked));
905            }
906        }
907
908        if format_version >= STORE_VERSION_V8 {
909            buf.push(u8::from(entity.has_explicit_logical_id()));
910            if entity.has_explicit_logical_id() {
911                write_varu64(buf, entity.logical_id().raw());
912            }
913        }
914        if format_version >= STORE_VERSION_V9 {
915            write_varu64(buf, entity.xmin);
916            write_varu64(buf, entity.xmax);
917        }
918    }
919
920    /// Read a Value from binary buffer
921    /// Type bytes: 0=Null, 1=Boolean, 2=Integer, 3=UnsignedInteger, 4=Float,
922    /// 5=Text, 6=Blob, 7=Timestamp, 8=Duration, 9=IpAddr, 10=MacAddr,
923    /// 11=Vector, 12=Json, 13=Uuid, 14=NodeRef, 15=EdgeRef, 16=VectorRef, 17=RowRef
924    fn read_value_binary(buf: &[u8], pos: &mut usize) -> Result<Value, Box<dyn std::error::Error>> {
925        use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
926
927        let type_byte = buf[*pos];
928        *pos += 1;
929
930        Ok(match type_byte {
931            0 => Value::Null,
932            1 => {
933                let b = buf[*pos] != 0;
934                *pos += 1;
935                Value::Boolean(b)
936            }
937            2 => {
938                let val = i64::from_le_bytes([
939                    buf[*pos],
940                    buf[*pos + 1],
941                    buf[*pos + 2],
942                    buf[*pos + 3],
943                    buf[*pos + 4],
944                    buf[*pos + 5],
945                    buf[*pos + 6],
946                    buf[*pos + 7],
947                ]);
948                *pos += 8;
949                Value::Integer(val)
950            }
951            3 => {
952                let val = u64::from_le_bytes([
953                    buf[*pos],
954                    buf[*pos + 1],
955                    buf[*pos + 2],
956                    buf[*pos + 3],
957                    buf[*pos + 4],
958                    buf[*pos + 5],
959                    buf[*pos + 6],
960                    buf[*pos + 7],
961                ]);
962                *pos += 8;
963                Value::UnsignedInteger(val)
964            }
965            4 => {
966                let val = f64::from_le_bytes([
967                    buf[*pos],
968                    buf[*pos + 1],
969                    buf[*pos + 2],
970                    buf[*pos + 3],
971                    buf[*pos + 4],
972                    buf[*pos + 5],
973                    buf[*pos + 6],
974                    buf[*pos + 7],
975                ]);
976                *pos += 8;
977                Value::Float(val)
978            }
979            5 => {
980                let len = Self::read_varu32_safe(buf, pos)?;
981                let s = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
982                *pos += len;
983                Value::text(s)
984            }
985            6 => {
986                let len = Self::read_varu32_safe(buf, pos)?;
987                let bytes = buf[*pos..*pos + len].to_vec();
988                *pos += len;
989                Value::Blob(bytes)
990            }
991            7 => {
992                let val = i64::from_le_bytes([
993                    buf[*pos],
994                    buf[*pos + 1],
995                    buf[*pos + 2],
996                    buf[*pos + 3],
997                    buf[*pos + 4],
998                    buf[*pos + 5],
999                    buf[*pos + 6],
1000                    buf[*pos + 7],
1001                ]);
1002                *pos += 8;
1003                Value::Timestamp(val)
1004            }
1005            8 => {
1006                let val = i64::from_le_bytes([
1007                    buf[*pos],
1008                    buf[*pos + 1],
1009                    buf[*pos + 2],
1010                    buf[*pos + 3],
1011                    buf[*pos + 4],
1012                    buf[*pos + 5],
1013                    buf[*pos + 6],
1014                    buf[*pos + 7],
1015                ]);
1016                *pos += 8;
1017                Value::Duration(val)
1018            }
1019            9 => {
1020                // IpAddr: first byte = version (4 or 6)
1021                let version = buf[*pos];
1022                *pos += 1;
1023                if version == 4 {
1024                    let octets = [buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]];
1025                    *pos += 4;
1026                    Value::IpAddr(IpAddr::V4(Ipv4Addr::from(octets)))
1027                } else {
1028                    let mut octets = [0u8; 16];
1029                    octets.copy_from_slice(&buf[*pos..*pos + 16]);
1030                    *pos += 16;
1031                    Value::IpAddr(IpAddr::V6(Ipv6Addr::from(octets)))
1032                }
1033            }
1034            10 => {
1035                let mut mac = [0u8; 6];
1036                mac.copy_from_slice(&buf[*pos..*pos + 6]);
1037                *pos += 6;
1038                Value::MacAddr(mac)
1039            }
1040            11 => {
1041                let len = Self::read_varu32_safe(buf, pos)?;
1042                let mut vector = Vec::with_capacity(len);
1043                for _ in 0..len {
1044                    let bytes = [buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]];
1045                    *pos += 4;
1046                    vector.push(f32::from_le_bytes(bytes));
1047                }
1048                Value::Vector(vector)
1049            }
1050            12 => {
1051                let len = Self::read_varu32_safe(buf, pos)?;
1052                let bytes = buf[*pos..*pos + len].to_vec();
1053                *pos += len;
1054                Value::Json(bytes)
1055            }
1056            13 => {
1057                let mut uuid = [0u8; 16];
1058                uuid.copy_from_slice(&buf[*pos..*pos + 16]);
1059                *pos += 16;
1060                Value::Uuid(uuid)
1061            }
1062            14 => {
1063                let len = Self::read_varu32_safe(buf, pos)?;
1064                let s = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1065                *pos += len;
1066                Value::NodeRef(s)
1067            }
1068            15 => {
1069                let len = Self::read_varu32_safe(buf, pos)?;
1070                let s = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1071                *pos += len;
1072                Value::EdgeRef(s)
1073            }
1074            16 => {
1075                let len = Self::read_varu32_safe(buf, pos)?;
1076                let s = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1077                *pos += len;
1078                let id = u64::from_le_bytes([
1079                    buf[*pos],
1080                    buf[*pos + 1],
1081                    buf[*pos + 2],
1082                    buf[*pos + 3],
1083                    buf[*pos + 4],
1084                    buf[*pos + 5],
1085                    buf[*pos + 6],
1086                    buf[*pos + 7],
1087                ]);
1088                *pos += 8;
1089                Value::VectorRef(s, id)
1090            }
1091            17 => {
1092                let len = Self::read_varu32_safe(buf, pos)?;
1093                let s = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1094                *pos += len;
1095                let id = u64::from_le_bytes([
1096                    buf[*pos],
1097                    buf[*pos + 1],
1098                    buf[*pos + 2],
1099                    buf[*pos + 3],
1100                    buf[*pos + 4],
1101                    buf[*pos + 5],
1102                    buf[*pos + 6],
1103                    buf[*pos + 7],
1104                ]);
1105                *pos += 8;
1106                Value::RowRef(s, id)
1107            }
1108            18 => {
1109                let rgb = [buf[*pos], buf[*pos + 1], buf[*pos + 2]];
1110                *pos += 3;
1111                Value::Color(rgb)
1112            }
1113            19 => {
1114                let len = Self::read_varu32_safe(buf, pos)?;
1115                let s = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1116                *pos += len;
1117                Value::Email(s)
1118            }
1119            20 => {
1120                let len = Self::read_varu32_safe(buf, pos)?;
1121                let s = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1122                *pos += len;
1123                Value::Url(s)
1124            }
1125            21 => {
1126                let val = u64::from_le_bytes([
1127                    buf[*pos],
1128                    buf[*pos + 1],
1129                    buf[*pos + 2],
1130                    buf[*pos + 3],
1131                    buf[*pos + 4],
1132                    buf[*pos + 5],
1133                    buf[*pos + 6],
1134                    buf[*pos + 7],
1135                ]);
1136                *pos += 8;
1137                Value::Phone(val)
1138            }
1139            22 => {
1140                let val =
1141                    u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1142                *pos += 4;
1143                Value::Semver(val)
1144            }
1145            23 => {
1146                let ip =
1147                    u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1148                *pos += 4;
1149                let prefix = buf[*pos];
1150                *pos += 1;
1151                Value::Cidr(ip, prefix)
1152            }
1153            24 => {
1154                let val =
1155                    i32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1156                *pos += 4;
1157                Value::Date(val)
1158            }
1159            25 => {
1160                let val =
1161                    u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1162                *pos += 4;
1163                Value::Time(val)
1164            }
1165            26 => {
1166                let val = i64::from_le_bytes([
1167                    buf[*pos],
1168                    buf[*pos + 1],
1169                    buf[*pos + 2],
1170                    buf[*pos + 3],
1171                    buf[*pos + 4],
1172                    buf[*pos + 5],
1173                    buf[*pos + 6],
1174                    buf[*pos + 7],
1175                ]);
1176                *pos += 8;
1177                Value::Decimal(val)
1178            }
1179            27 => {
1180                let val = buf[*pos];
1181                *pos += 1;
1182                Value::EnumValue(val)
1183            }
1184            28 => {
1185                let len = Self::read_varu32_safe(buf, pos)?;
1186                let mut elems = Vec::with_capacity(len);
1187                for _ in 0..len {
1188                    elems.push(Self::read_value_binary(buf, pos)?);
1189                }
1190                Value::Array(elems)
1191            }
1192            29 => {
1193                let val = i64::from_le_bytes([
1194                    buf[*pos],
1195                    buf[*pos + 1],
1196                    buf[*pos + 2],
1197                    buf[*pos + 3],
1198                    buf[*pos + 4],
1199                    buf[*pos + 5],
1200                    buf[*pos + 6],
1201                    buf[*pos + 7],
1202                ]);
1203                *pos += 8;
1204                Value::TimestampMs(val)
1205            }
1206            30 => {
1207                let val =
1208                    u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1209                *pos += 4;
1210                Value::Ipv4(val)
1211            }
1212            31 => {
1213                let mut bytes = [0u8; 16];
1214                bytes.copy_from_slice(&buf[*pos..*pos + 16]);
1215                *pos += 16;
1216                Value::Ipv6(bytes)
1217            }
1218            32 => {
1219                let ip =
1220                    u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1221                *pos += 4;
1222                let mask =
1223                    u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1224                *pos += 4;
1225                Value::Subnet(ip, mask)
1226            }
1227            33 => {
1228                let val = u16::from_le_bytes([buf[*pos], buf[*pos + 1]]);
1229                *pos += 2;
1230                Value::Port(val)
1231            }
1232            34 => {
1233                let val =
1234                    i32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1235                *pos += 4;
1236                Value::Latitude(val)
1237            }
1238            35 => {
1239                let val =
1240                    i32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1241                *pos += 4;
1242                Value::Longitude(val)
1243            }
1244            36 => {
1245                let lat =
1246                    i32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1247                *pos += 4;
1248                let lon =
1249                    i32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1250                *pos += 4;
1251                Value::GeoPoint(lat, lon)
1252            }
1253            37 => {
1254                let c = [buf[*pos], buf[*pos + 1]];
1255                *pos += 2;
1256                Value::Country2(c)
1257            }
1258            38 => {
1259                let c = [buf[*pos], buf[*pos + 1], buf[*pos + 2]];
1260                *pos += 3;
1261                Value::Country3(c)
1262            }
1263            39 => {
1264                let c = [buf[*pos], buf[*pos + 1]];
1265                *pos += 2;
1266                Value::Lang2(c)
1267            }
1268            40 => {
1269                let c = [
1270                    buf[*pos],
1271                    buf[*pos + 1],
1272                    buf[*pos + 2],
1273                    buf[*pos + 3],
1274                    buf[*pos + 4],
1275                ];
1276                *pos += 5;
1277                Value::Lang5(c)
1278            }
1279            41 => {
1280                let c = [buf[*pos], buf[*pos + 1], buf[*pos + 2]];
1281                *pos += 3;
1282                Value::Currency(c)
1283            }
1284            42 => {
1285                let rgba = [buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]];
1286                *pos += 4;
1287                Value::ColorAlpha(rgba)
1288            }
1289            43 => {
1290                let val = i64::from_le_bytes([
1291                    buf[*pos],
1292                    buf[*pos + 1],
1293                    buf[*pos + 2],
1294                    buf[*pos + 3],
1295                    buf[*pos + 4],
1296                    buf[*pos + 5],
1297                    buf[*pos + 6],
1298                    buf[*pos + 7],
1299                ]);
1300                *pos += 8;
1301                Value::BigInt(val)
1302            }
1303            44 => {
1304                let col_len = Self::read_varu32_safe(buf, pos)?;
1305                let col = String::from_utf8(buf[*pos..*pos + col_len].to_vec())?;
1306                *pos += col_len;
1307                let key_len = Self::read_varu32_safe(buf, pos)?;
1308                let key = String::from_utf8(buf[*pos..*pos + key_len].to_vec())?;
1309                *pos += key_len;
1310                Value::KeyRef(col, key)
1311            }
1312            45 => {
1313                let col_len = Self::read_varu32_safe(buf, pos)?;
1314                let col = String::from_utf8(buf[*pos..*pos + col_len].to_vec())?;
1315                *pos += col_len;
1316                let id = u64::from_le_bytes([
1317                    buf[*pos],
1318                    buf[*pos + 1],
1319                    buf[*pos + 2],
1320                    buf[*pos + 3],
1321                    buf[*pos + 4],
1322                    buf[*pos + 5],
1323                    buf[*pos + 6],
1324                    buf[*pos + 7],
1325                ]);
1326                *pos += 8;
1327                Value::DocRef(col, id)
1328            }
1329            46 => {
1330                let len = Self::read_varu32_safe(buf, pos)?;
1331                let name = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1332                *pos += len;
1333                Value::TableRef(name)
1334            }
1335            47 => {
1336                let val =
1337                    u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
1338                *pos += 4;
1339                Value::PageRef(val)
1340            }
1341            48 => {
1342                let len = Self::read_varu32_safe(buf, pos)?;
1343                let bytes = buf[*pos..*pos + len].to_vec();
1344                *pos += len;
1345                Value::Secret(bytes)
1346            }
1347            49 => {
1348                let len = Self::read_varu32_safe(buf, pos)?;
1349                let hash = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1350                *pos += len;
1351                Value::Password(hash)
1352            }
1353            50 => {
1354                let len = Self::read_varu32_safe(buf, pos)?;
1355                let code = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1356                *pos += len;
1357                Value::AssetCode(code)
1358            }
1359            51 => {
1360                let len = Self::read_varu32_safe(buf, pos)?;
1361                let asset_code = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1362                *pos += len;
1363                let scale = buf[*pos];
1364                *pos += 1;
1365                let minor_units = i64::from_le_bytes([
1366                    buf[*pos],
1367                    buf[*pos + 1],
1368                    buf[*pos + 2],
1369                    buf[*pos + 3],
1370                    buf[*pos + 4],
1371                    buf[*pos + 5],
1372                    buf[*pos + 6],
1373                    buf[*pos + 7],
1374                ]);
1375                *pos += 8;
1376                Value::Money {
1377                    asset_code,
1378                    minor_units,
1379                    scale,
1380                }
1381            }
1382            52 => {
1383                let len = Self::read_varu32_safe(buf, pos)?;
1384                let value = String::from_utf8(buf[*pos..*pos + len].to_vec())?;
1385                *pos += len;
1386                Value::DecimalText(value)
1387            }
1388            // C3 TOAST: compressed Text (0x85) and Blob (0x86).
1389            0x85 => {
1390                let orig_len = Self::read_varu32_safe(buf, pos)?;
1391                let comp_len = Self::read_varu32_safe(buf, pos)?;
1392                let compressed = &buf[*pos..*pos + comp_len];
1393                *pos += comp_len;
1394                let mut out = vec![0u8; orig_len];
1395                zstd::bulk::decompress_to_buffer(compressed, &mut out)
1396                    .map_err(|e| format!("C3 Text decompress: {e}"))?;
1397                Value::text(String::from_utf8(out).map_err(|e| format!("C3 Text UTF-8: {e}"))?)
1398            }
1399            0x86 => {
1400                let orig_len = Self::read_varu32_safe(buf, pos)?;
1401                let comp_len = Self::read_varu32_safe(buf, pos)?;
1402                let compressed = &buf[*pos..*pos + comp_len];
1403                *pos += comp_len;
1404                let mut out = vec![0u8; orig_len];
1405                zstd::bulk::decompress_to_buffer(compressed, &mut out)
1406                    .map_err(|e| format!("C3 Blob decompress: {e}"))?;
1407                Value::Blob(out)
1408            }
1409            _ => return Err(format!("Unknown Value type: {}", type_byte).into()),
1410        })
1411    }
1412
1413    /// Write a Value to binary buffer
1414    /// Type bytes: 0=Null, 1=Boolean, 2=Integer, 3=UnsignedInteger, 4=Float,
1415    /// 5=Text, 6=Blob, 7=Timestamp, 8=Duration, 9=IpAddr, 10=MacAddr,
1416    /// 11=Vector, 12=Json, 13=Uuid, 14=NodeRef, 15=EdgeRef, 16=VectorRef, 17=RowRef
1417    fn write_value_binary(buf: &mut Vec<u8>, value: &Value) {
1418        use std::net::IpAddr;
1419
1420        match value {
1421            Value::Null => buf.push(0),
1422            Value::Boolean(b) => {
1423                buf.push(1);
1424                buf.push(if *b { 1 } else { 0 });
1425            }
1426            Value::Integer(i) => {
1427                buf.push(2);
1428                buf.extend_from_slice(&i.to_le_bytes());
1429            }
1430            Value::UnsignedInteger(u) => {
1431                buf.push(3);
1432                buf.extend_from_slice(&u.to_le_bytes());
1433            }
1434            Value::Float(f) => {
1435                buf.push(4);
1436                buf.extend_from_slice(&f.to_le_bytes());
1437            }
1438            Value::Text(s) => {
1439                // C3 TOAST: compress Text values > 2 KiB with zstd.
1440                // Type 0x85 = compressed Text; type 5 = uncompressed (existing).
1441                // Layout for 0x85: [0x85][orig_len: varuint][comp_len: varuint][compressed bytes]
1442                const TEXT_COMPRESS_THRESHOLD: usize = 2048;
1443                if s.len() >= TEXT_COMPRESS_THRESHOLD {
1444                    if let Ok(compressed) = zstd::bulk::compress(s.as_bytes(), 3) {
1445                        if compressed.len() < s.len() {
1446                            buf.push(0x85); // compressed Text marker
1447                            write_varu32(buf, s.len() as u32); // orig_len (decompression hint)
1448                            write_varu32(buf, compressed.len() as u32);
1449                            buf.extend_from_slice(&compressed);
1450                            return; // written — skip uncompressed path
1451                        }
1452                    }
1453                }
1454                buf.push(5);
1455                write_varu32(buf, s.len() as u32);
1456                buf.extend_from_slice(s.as_bytes());
1457            }
1458            Value::Blob(bytes) => {
1459                // C3 TOAST: compress Blob values > 2 KiB with zstd.
1460                // Type 0x86 = compressed Blob; type 6 = uncompressed (existing).
1461                const BLOB_COMPRESS_THRESHOLD: usize = 2048;
1462                if bytes.len() >= BLOB_COMPRESS_THRESHOLD {
1463                    if let Ok(compressed) = zstd::bulk::compress(bytes.as_slice(), 3) {
1464                        if compressed.len() < bytes.len() {
1465                            buf.push(0x86); // compressed Blob marker
1466                            write_varu32(buf, bytes.len() as u32);
1467                            write_varu32(buf, compressed.len() as u32);
1468                            buf.extend_from_slice(&compressed);
1469                            return;
1470                        }
1471                    }
1472                }
1473                buf.push(6);
1474                write_varu32(buf, bytes.len() as u32);
1475                buf.extend_from_slice(bytes);
1476            }
1477            Value::Timestamp(t) => {
1478                buf.push(7);
1479                buf.extend_from_slice(&t.to_le_bytes());
1480            }
1481            Value::Duration(d) => {
1482                buf.push(8);
1483                buf.extend_from_slice(&d.to_le_bytes());
1484            }
1485            Value::IpAddr(ip) => {
1486                buf.push(9);
1487                match ip {
1488                    IpAddr::V4(v4) => {
1489                        buf.push(4);
1490                        buf.extend_from_slice(&v4.octets());
1491                    }
1492                    IpAddr::V6(v6) => {
1493                        buf.push(6);
1494                        buf.extend_from_slice(&v6.octets());
1495                    }
1496                }
1497            }
1498            Value::MacAddr(mac) => {
1499                buf.push(10);
1500                buf.extend_from_slice(mac);
1501            }
1502            Value::Vector(vec) => {
1503                buf.push(11);
1504                write_varu32(buf, vec.len() as u32);
1505                for f in vec {
1506                    buf.extend_from_slice(&f.to_le_bytes());
1507                }
1508            }
1509            Value::Json(bytes) => {
1510                buf.push(12);
1511                write_varu32(buf, bytes.len() as u32);
1512                buf.extend_from_slice(bytes);
1513            }
1514            Value::Uuid(uuid) => {
1515                buf.push(13);
1516                buf.extend_from_slice(uuid);
1517            }
1518            Value::NodeRef(s) => {
1519                buf.push(14);
1520                write_varu32(buf, s.len() as u32);
1521                buf.extend_from_slice(s.as_bytes());
1522            }
1523            Value::EdgeRef(s) => {
1524                buf.push(15);
1525                write_varu32(buf, s.len() as u32);
1526                buf.extend_from_slice(s.as_bytes());
1527            }
1528            Value::VectorRef(s, id) => {
1529                buf.push(16);
1530                write_varu32(buf, s.len() as u32);
1531                buf.extend_from_slice(s.as_bytes());
1532                buf.extend_from_slice(&id.to_le_bytes());
1533            }
1534            Value::RowRef(s, id) => {
1535                buf.push(17);
1536                write_varu32(buf, s.len() as u32);
1537                buf.extend_from_slice(s.as_bytes());
1538                buf.extend_from_slice(&id.to_le_bytes());
1539            }
1540            Value::Color(rgb) => {
1541                buf.push(18);
1542                buf.extend_from_slice(rgb);
1543            }
1544            Value::Email(s) => {
1545                buf.push(19);
1546                write_varu32(buf, s.len() as u32);
1547                buf.extend_from_slice(s.as_bytes());
1548            }
1549            Value::Url(s) => {
1550                buf.push(20);
1551                write_varu32(buf, s.len() as u32);
1552                buf.extend_from_slice(s.as_bytes());
1553            }
1554            Value::Phone(n) => {
1555                buf.push(21);
1556                buf.extend_from_slice(&n.to_le_bytes());
1557            }
1558            Value::Semver(v) => {
1559                buf.push(22);
1560                buf.extend_from_slice(&v.to_le_bytes());
1561            }
1562            Value::Cidr(ip, prefix) => {
1563                buf.push(23);
1564                buf.extend_from_slice(&ip.to_le_bytes());
1565                buf.push(*prefix);
1566            }
1567            Value::Date(d) => {
1568                buf.push(24);
1569                buf.extend_from_slice(&d.to_le_bytes());
1570            }
1571            Value::Time(t) => {
1572                buf.push(25);
1573                buf.extend_from_slice(&t.to_le_bytes());
1574            }
1575            Value::Decimal(v) => {
1576                buf.push(26);
1577                buf.extend_from_slice(&v.to_le_bytes());
1578            }
1579            Value::EnumValue(i) => {
1580                buf.push(27);
1581                buf.push(*i);
1582            }
1583            Value::Array(elems) => {
1584                buf.push(28);
1585                write_varu32(buf, elems.len() as u32);
1586                for elem in elems {
1587                    Self::write_value_binary(buf, elem);
1588                }
1589            }
1590            Value::TimestampMs(v) => {
1591                buf.push(29);
1592                buf.extend_from_slice(&v.to_le_bytes());
1593            }
1594            Value::Ipv4(v) => {
1595                buf.push(30);
1596                buf.extend_from_slice(&v.to_le_bytes());
1597            }
1598            Value::Ipv6(bytes) => {
1599                buf.push(31);
1600                buf.extend_from_slice(bytes);
1601            }
1602            Value::Subnet(ip, mask) => {
1603                buf.push(32);
1604                buf.extend_from_slice(&ip.to_le_bytes());
1605                buf.extend_from_slice(&mask.to_le_bytes());
1606            }
1607            Value::Port(v) => {
1608                buf.push(33);
1609                buf.extend_from_slice(&v.to_le_bytes());
1610            }
1611            Value::Latitude(v) => {
1612                buf.push(34);
1613                buf.extend_from_slice(&v.to_le_bytes());
1614            }
1615            Value::Longitude(v) => {
1616                buf.push(35);
1617                buf.extend_from_slice(&v.to_le_bytes());
1618            }
1619            Value::GeoPoint(lat, lon) => {
1620                buf.push(36);
1621                buf.extend_from_slice(&lat.to_le_bytes());
1622                buf.extend_from_slice(&lon.to_le_bytes());
1623            }
1624            Value::Country2(c) => {
1625                buf.push(37);
1626                buf.extend_from_slice(c);
1627            }
1628            Value::Country3(c) => {
1629                buf.push(38);
1630                buf.extend_from_slice(c);
1631            }
1632            Value::Lang2(c) => {
1633                buf.push(39);
1634                buf.extend_from_slice(c);
1635            }
1636            Value::Lang5(c) => {
1637                buf.push(40);
1638                buf.extend_from_slice(c);
1639            }
1640            Value::Currency(c) => {
1641                buf.push(41);
1642                buf.extend_from_slice(c);
1643            }
1644            Value::AssetCode(code) => {
1645                buf.push(50);
1646                write_varu32(buf, code.len() as u32);
1647                buf.extend_from_slice(code.as_bytes());
1648            }
1649            Value::Money {
1650                asset_code,
1651                minor_units,
1652                scale,
1653            } => {
1654                buf.push(51);
1655                write_varu32(buf, asset_code.len() as u32);
1656                buf.extend_from_slice(asset_code.as_bytes());
1657                buf.push(*scale);
1658                buf.extend_from_slice(&minor_units.to_le_bytes());
1659            }
1660            Value::ColorAlpha(rgba) => {
1661                buf.push(42);
1662                buf.extend_from_slice(rgba);
1663            }
1664            Value::BigInt(v) => {
1665                buf.push(43);
1666                buf.extend_from_slice(&v.to_le_bytes());
1667            }
1668            Value::KeyRef(col, key) => {
1669                buf.push(44);
1670                write_varu32(buf, col.len() as u32);
1671                buf.extend_from_slice(col.as_bytes());
1672                write_varu32(buf, key.len() as u32);
1673                buf.extend_from_slice(key.as_bytes());
1674            }
1675            Value::DocRef(col, id) => {
1676                buf.push(45);
1677                write_varu32(buf, col.len() as u32);
1678                buf.extend_from_slice(col.as_bytes());
1679                buf.extend_from_slice(&id.to_le_bytes());
1680            }
1681            Value::TableRef(name) => {
1682                buf.push(46);
1683                write_varu32(buf, name.len() as u32);
1684                buf.extend_from_slice(name.as_bytes());
1685            }
1686            Value::PageRef(page_id) => {
1687                buf.push(47);
1688                buf.extend_from_slice(&page_id.to_le_bytes());
1689            }
1690            Value::Secret(bytes) => {
1691                buf.push(48);
1692                write_varu32(buf, bytes.len() as u32);
1693                buf.extend_from_slice(bytes);
1694            }
1695            Value::Password(hash) => {
1696                buf.push(49);
1697                write_varu32(buf, hash.len() as u32);
1698                buf.extend_from_slice(hash.as_bytes());
1699            }
1700            Value::DecimalText(value) => {
1701                buf.push(52);
1702                write_varu32(buf, value.len() as u32);
1703                buf.extend_from_slice(value.as_bytes());
1704            }
1705        }
1706    }
1707}
1708
1709#[cfg(test)]
1710mod aux_metadata_dump_tests {
1711    use super::*;
1712    use std::collections::HashMap;
1713
1714    #[test]
1715    fn snapshot_load_preserves_existing_wal_and_restores_future_appends() {
1716        let directory = tempfile::tempdir().expect("directory");
1717        let path = directory.path().join("snapshot.rdb");
1718        let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
1719        let old =
1720            UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text("snapshot")]);
1721        store.insert_auto("rows", old).expect("snapshot row");
1722        let snapshot = store.to_binary_dump_bytes();
1723        crate::storage::EmbeddedRdbArtifact::create_with_snapshot(&path, &snapshot)
1724            .expect("artifact");
1725        let newer = UnifiedEntity::table_row(
1726            EntityId::new(1),
1727            "rows",
1728            1,
1729            vec![Value::text("durable WAL")],
1730        );
1731        let action = StoreWalAction::upsert_entity("rows", &newer, None, STORE_VERSION_CURRENT);
1732        UnifiedStore::with_config(UnifiedStoreConfig::default().with_embedded_wal_path(&path))
1733            .finish_paged_write([action])
1734            .expect("durable update after snapshot");
1735        let before = std::fs::read(&path).expect("original artifact bytes");
1736        let loaded = UnifiedStore::load_from_bytes_with_config(
1737            &snapshot,
1738            UnifiedStoreConfig::default().with_embedded_wal_path(&path),
1739        )
1740        .expect("reconstruct snapshot");
1741        assert_eq!(std::fs::read(&path).expect("artifact after load"), before);
1742        assert_eq!(
1743            loaded.config().embedded_wal_path.as_deref(),
1744            Some(path.as_path())
1745        );
1746
1747        let generation = crate::storage::EmbeddedRdbArtifact::open(&path)
1748            .expect("boundary before future write")
1749            .selected_superblock
1750            .generation;
1751        loaded
1752            .insert_auto(
1753                "rows",
1754                UnifiedEntity::table_row(
1755                    EntityId::new(2),
1756                    "rows",
1757                    2,
1758                    vec![Value::text("future write")],
1759                ),
1760            )
1761            .expect("future writes still append WAL");
1762        assert_eq!(
1763            crate::storage::EmbeddedRdbArtifact::open(&path)
1764                .expect("boundary after future write")
1765                .selected_superblock
1766                .generation,
1767            generation + 1
1768        );
1769    }
1770
1771    #[test]
1772    fn aux_metadata_round_trips_through_binary_dump() {
1773        let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
1774        store.create_collection("k").expect("create collection");
1775        store.set_aux_metadata(b"contract-blob".to_vec());
1776
1777        let bytes = store.to_binary_dump_bytes();
1778        let reloaded = UnifiedStore::load_from_bytes(&bytes).expect("reload dump");
1779
1780        assert_eq!(reloaded.aux_metadata(), b"contract-blob".to_vec());
1781    }
1782
1783    #[test]
1784    fn empty_aux_metadata_round_trips_as_empty() {
1785        let store = UnifiedStore::with_config(UnifiedStoreConfig::default());
1786        let bytes = store.to_binary_dump_bytes();
1787        let reloaded = UnifiedStore::load_from_bytes(&bytes).expect("reload dump");
1788
1789        assert!(reloaded.aux_metadata().is_empty());
1790    }
1791
1792    #[test]
1793    fn interned_timeseries_series_reduces_repeated_tag_storage() {
1794        let tags = HashMap::from([
1795            (
1796                "host".to_string(),
1797                "srv-very-long-hostname-0001".to_string(),
1798            ),
1799            ("region".to_string(), "us-east-1".to_string()),
1800            ("service".to_string(), "checkout-api".to_string()),
1801        ]);
1802        let inline_bytes: usize = (0..100)
1803            .map(|timestamp_ns| {
1804                let entity = UnifiedEntity::new(
1805                    EntityId::new(0),
1806                    EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind {
1807                        series: "cpu_metrics".to_string(),
1808                        metric: "cpu.idle".to_string(),
1809                    })),
1810                    EntityData::TimeSeries(crate::storage::TimeSeriesData {
1811                        metric: "cpu.idle".to_string(),
1812                        series_id: None,
1813                        timestamp_ns,
1814                        value: 94.8,
1815                        tags: tags.clone(),
1816                        fields: HashMap::new(),
1817                    }),
1818                );
1819                UnifiedStore::serialize_entity_record(&entity, None, reddb_file::STORE_VERSION_V10)
1820                    .len()
1821            })
1822            .sum();
1823
1824        let mut dictionary_fields = HashMap::new();
1825        dictionary_fields.insert("collection".to_string(), Value::text("cpu_metrics"));
1826        dictionary_fields.insert("series_id".to_string(), Value::UnsignedInteger(0));
1827        dictionary_fields.insert("metric".to_string(), Value::text("cpu.idle"));
1828        dictionary_fields.insert(
1829            "canonical_tags".to_string(),
1830            Value::text(
1831                r#"{"host":"srv-very-long-hostname-0001","region":"us-east-1","service":"checkout-api"}"#,
1832            ),
1833        );
1834        dictionary_fields.insert(
1835            "tags".to_string(),
1836            Value::Json(
1837                br#"{"host":"srv-very-long-hostname-0001","region":"us-east-1","service":"checkout-api"}"#
1838                    .to_vec(),
1839            ),
1840        );
1841        let dictionary = UnifiedEntity::new(
1842            EntityId::new(0),
1843            EntityKind::TableRow {
1844                table: Arc::from("red_timeseries_series"),
1845                row_id: 0,
1846            },
1847            EntityData::Row(RowData {
1848                columns: Vec::new(),
1849                named: Some(dictionary_fields),
1850                schema: None,
1851            }),
1852        );
1853        let dictionary_bytes =
1854            UnifiedStore::serialize_entity_record(&dictionary, None, STORE_VERSION_V11).len();
1855        let interned_bytes = dictionary_bytes
1856            + (0..100)
1857                .map(|timestamp_ns| {
1858                    let entity = UnifiedEntity::new(
1859                        EntityId::new(0),
1860                        EntityKind::TimeSeriesPoint(Box::new(TimeSeriesPointKind {
1861                            series: "cpu_metrics".to_string(),
1862                            metric: "cpu.idle".to_string(),
1863                        })),
1864                        EntityData::TimeSeries(crate::storage::TimeSeriesData {
1865                            metric: "cpu.idle".to_string(),
1866                            series_id: Some(0),
1867                            timestamp_ns,
1868                            value: 94.8,
1869                            tags: HashMap::new(),
1870                            fields: HashMap::new(),
1871                        }),
1872                    );
1873                    UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11).len()
1874                })
1875                .sum::<usize>();
1876
1877        assert!(
1878            interned_bytes < inline_bytes,
1879            "interned series storage should shrink repeated tags: inline={inline_bytes}, interned={interned_bytes}"
1880        );
1881    }
1882}