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