Skip to main content

reddb_types/
document_body_codec.rs

1//! Native binary container for document bodies (PRD-1398, ADR-0063).
2//!
3//! Encodes a named-field document (`&[(&str, &Value)]`) into a compact binary
4//! container with a **header offset table** that enables O(1) access to any
5//! top-level field value without decoding the entire document.
6//!
7//! ## Format — version 1
8//!
9//! ```text
10//! [0..4]  magic        = b"RDOC"
11//! [4]     version      = 0x01
12//! [5..7]  num_fields   : u16 LE          (max 65 535 fields per document)
13//! [7..]   offset table : num_fields × [key_len: u16 LE, val_offset: u32 LE]
14//!         keys section : concatenated UTF-8 field name bytes (key_len[i] each)
15//!         values section: concatenated value_codec-encoded bytes
16//! ```
17//!
18//! `val_offset` is an **absolute** byte offset from byte 0 of the container,
19//! allowing a single value to be decoded with [`decode_value_at_offset`] after
20//! reading only the header — the rest of the payload stays untouched.
21//!
22//! ## Flag-dark
23//!
24//! This codec is compiled and tested but not yet wired into any storage or
25//! query path.  Behaviour cutover happens in a later PRD-1398 slice.
26
27use crate::key_dictionary::KeyDictionary;
28use crate::types::Value;
29use crate::value_codec;
30
31/// Magic bytes at the start of every document body container.
32pub const MAGIC: &[u8; 4] = b"RDOC";
33
34/// Format version byte for the plain (inline-keys-only) container.
35pub const VERSION: u8 = 0x01;
36
37/// Format version byte for the dictionary-aware container (PRD-1398).
38///
39/// Identical layout to v1 except each offset-table entry carries a key *kind*
40/// tag: a field name is either a key-id into the per-collection
41/// [`KeyDictionary`] (common keys) or stored inline (rare/unique keys).
42pub const VERSION_DICT: u8 = 0x02;
43
44/// Byte size of one entry in the offset table: u16 key_len + u32 val_offset.
45const ENTRY_SIZE: usize = 6;
46
47/// Byte size of one v2 offset-table entry: u8 key_kind + u32 key_ref + u32 val_offset.
48///
49/// The key-id is stored as a fixed `u32` (rather than a varint) so the table
50/// keeps a fixed stride and field access stays O(1); homogeneous collections
51/// never approach `u32::MAX` distinct common keys.
52const DICT_ENTRY_SIZE: usize = 9;
53
54/// `key_kind` tag: the field name is a key-id into the [`KeyDictionary`].
55const KEY_KIND_DICT: u8 = 0x00;
56
57/// `key_kind` tag: the field name is stored inline in the keys section.
58const KEY_KIND_INLINE: u8 = 0x01;
59
60/// One parsed v2 offset-table entry: `(key_kind, key_ref, val_offset)`.
61///
62/// `key_ref` is a dictionary key-id when `key_kind == KEY_KIND_DICT`, or the
63/// inline key length when `key_kind == KEY_KIND_INLINE`.
64pub type DictEntry = (u8, u32, u32);
65
66/// Errors produced by the document body codec.
67#[derive(Debug, PartialEq)]
68pub enum DocBodyError {
69    /// Buffer is too short to hold the header or offset table.
70    TruncatedData,
71    /// First 4 bytes do not match `b"RDOC"`.
72    BadMagic,
73    /// Version byte is not `0x01`.
74    UnsupportedVersion(u8),
75    /// A field name or value points outside the container buffer.
76    OffsetOutOfBounds,
77    /// A field name is not valid UTF-8.
78    InvalidFieldName,
79    /// The document has more than 65 535 fields, or a field name exceeds 65 535 bytes.
80    FieldLimitExceeded,
81    /// A dictionary key-id in the body is absent from the supplied dictionary.
82    UnknownKeyId(u32),
83    /// A v2 offset-table entry carried an unrecognised key-kind tag.
84    BadKeyKind(u8),
85    /// The underlying value codec rejected a value.
86    ValueCodecError(crate::types::ValueError),
87}
88
89impl From<crate::types::ValueError> for DocBodyError {
90    fn from(e: crate::types::ValueError) -> Self {
91        DocBodyError::ValueCodecError(e)
92    }
93}
94
95impl std::fmt::Display for DocBodyError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            Self::TruncatedData => write!(f, "document body: truncated data"),
99            Self::BadMagic => write!(f, "document body: bad magic bytes (expected RDOC)"),
100            Self::UnsupportedVersion(v) => write!(f, "document body: unsupported version {v}"),
101            Self::OffsetOutOfBounds => write!(f, "document body: offset points outside buffer"),
102            Self::InvalidFieldName => write!(f, "document body: field name is not valid UTF-8"),
103            Self::FieldLimitExceeded => {
104                write!(f, "document body: field or key-length limit exceeded")
105            }
106            Self::UnknownKeyId(id) => {
107                write!(f, "document body: key-id {id} not found in dictionary")
108            }
109            Self::BadKeyKind(k) => write!(f, "document body: unrecognised key kind {k:#04x}"),
110            Self::ValueCodecError(e) => write!(f, "document body: value codec error: {e}"),
111        }
112    }
113}
114
115impl std::error::Error for DocBodyError {}
116
117/// Encode `fields` as a document body container, appending bytes to `out`.
118///
119/// Fields are written in iteration order.  Duplicate field names are allowed
120/// (deduplication is the caller's responsibility).
121///
122/// Returns an error only if `fields.len() > 65535` or a field name is longer
123/// than 65535 bytes; both are pathological in practice.
124pub fn encode(fields: &[(&str, &Value)], out: &mut Vec<u8>) -> Result<(), DocBodyError> {
125    let n = fields.len();
126    if n > u16::MAX as usize {
127        return Err(DocBodyError::FieldLimitExceeded);
128    }
129
130    // Encode all keys and values into scratch buffers, tracking per-field
131    // sizes so we can compute absolute offsets before writing anything.
132    let mut key_buf: Vec<u8> = Vec::new();
133    let mut val_buf: Vec<u8> = Vec::new();
134    let mut key_lens: Vec<u16> = Vec::with_capacity(n);
135    let mut val_starts: Vec<u32> = Vec::with_capacity(n);
136
137    for (key, value) in fields {
138        let kb = key.as_bytes();
139        if kb.len() > u16::MAX as usize {
140            return Err(DocBodyError::FieldLimitExceeded);
141        }
142        key_lens.push(kb.len() as u16);
143        key_buf.extend_from_slice(kb);
144
145        val_starts.push(val_buf.len() as u32); // relative for now
146        value_codec::encode(value, &mut val_buf);
147    }
148
149    // Absolute offset of the values section within the final container:
150    //   magic(4) + version(1) + num_fields(2) + table(n * ENTRY_SIZE) + keys
151    let vals_abs_start = 4 + 1 + 2 + n * ENTRY_SIZE + key_buf.len();
152
153    // Write header
154    out.extend_from_slice(MAGIC);
155    out.push(VERSION);
156    out.extend_from_slice(&(n as u16).to_le_bytes());
157
158    // Write offset table
159    for i in 0..n {
160        let abs_val_offset = vals_abs_start + val_starts[i] as usize;
161        out.extend_from_slice(&key_lens[i].to_le_bytes());
162        out.extend_from_slice(&(abs_val_offset as u32).to_le_bytes());
163    }
164
165    // Write keys then values
166    out.extend_from_slice(&key_buf);
167    out.extend_from_slice(&val_buf);
168
169    Ok(())
170}
171
172/// Decode all fields from a document body container.
173///
174/// Fields are returned in the same order they were encoded.
175pub fn decode(data: &[u8]) -> Result<Vec<(String, Value)>, DocBodyError> {
176    let (n, table) = parse_header(data)?;
177    let header_end = 7 + n * ENTRY_SIZE; // where the keys section begins
178    let mut key_cursor = header_end;
179    let mut result = Vec::with_capacity(n);
180
181    for &(key_len, val_offset) in &table {
182        // Read field name
183        let key_end = key_cursor + key_len as usize;
184        if key_end > data.len() {
185            return Err(DocBodyError::OffsetOutOfBounds);
186        }
187        let key = std::str::from_utf8(&data[key_cursor..key_end])
188            .map_err(|_| DocBodyError::InvalidFieldName)?
189            .to_string();
190        key_cursor = key_end;
191
192        // Decode value by absolute offset
193        let value = decode_value_at_offset(data, val_offset)?;
194        result.push((key, value));
195    }
196
197    Ok(result)
198}
199
200/// Return the top-level field names in encode order without decoding any value.
201///
202/// Reads only the offset table and the keys section — the values section is
203/// never touched. Used to build a field-name index/bloom over a stored body
204/// (e.g. so a `WHERE`/projection on a promoted field can route to the body)
205/// without paying the cost of a full [`decode`].
206pub fn field_names(data: &[u8]) -> Result<Vec<String>, DocBodyError> {
207    let (n, table) = parse_header(data)?;
208    let header_end = 7 + n * ENTRY_SIZE; // where the keys section begins
209    let mut key_cursor = header_end;
210    let mut names = Vec::with_capacity(n);
211
212    for &(key_len, _val_offset) in &table {
213        let key_end = key_cursor + key_len as usize;
214        if key_end > data.len() {
215            return Err(DocBodyError::OffsetOutOfBounds);
216        }
217        let key = std::str::from_utf8(&data[key_cursor..key_end])
218            .map_err(|_| DocBodyError::InvalidFieldName)?
219            .to_string();
220        key_cursor = key_end;
221        names.push(key);
222    }
223
224    Ok(names)
225}
226
227/// Read a single field by name without decoding any other value.
228///
229/// Returns `None` when the field is absent.  Only the matching field's
230/// encoded bytes are passed to [`value_codec::decode`]; everything else
231/// is skipped as raw bytes.
232pub fn read_field_by_name(data: &[u8], name: &str) -> Result<Option<Value>, DocBodyError> {
233    let (n, table) = parse_header(data)?;
234    let header_end = 7 + n * ENTRY_SIZE;
235    let mut key_cursor = header_end;
236
237    for &(key_len, val_offset) in &table {
238        let key_end = key_cursor + key_len as usize;
239        if key_end > data.len() {
240            return Err(DocBodyError::OffsetOutOfBounds);
241        }
242        let key_bytes = &data[key_cursor..key_end];
243        key_cursor = key_end;
244
245        if key_bytes == name.as_bytes() {
246            let value = decode_value_at_offset(data, val_offset)?;
247            return Ok(Some(value));
248        }
249    }
250
251    Ok(None)
252}
253
254/// Decode a single value using its **absolute** byte offset within the
255/// container.
256///
257/// This is the O(1) access path once the caller has read the offset table
258/// (e.g. via [`parse_header`]).  No other field is decoded.
259pub fn decode_value_at_offset(data: &[u8], val_offset: u32) -> Result<Value, DocBodyError> {
260    let off = val_offset as usize;
261    if off >= data.len() {
262        return Err(DocBodyError::OffsetOutOfBounds);
263    }
264    let (value, _) = value_codec::decode(&data[off..])?;
265    Ok(value)
266}
267
268/// Parse the container header and return `(num_fields, offset_table)`.
269///
270/// Each table entry is `(key_len: u16, val_offset: u32)`.  The table may be
271/// empty for zero-field documents.
272///
273/// Does **not** validate that field names or values are within bounds — that
274/// is deferred to the decode functions that actually walk those sections.
275pub fn parse_header(data: &[u8]) -> Result<(usize, Vec<(u16, u32)>), DocBodyError> {
276    if data.len() < 7 {
277        return Err(DocBodyError::TruncatedData);
278    }
279    if &data[0..4] != MAGIC.as_slice() {
280        return Err(DocBodyError::BadMagic);
281    }
282    if data[4] != VERSION {
283        return Err(DocBodyError::UnsupportedVersion(data[4]));
284    }
285
286    let n = u16::from_le_bytes([data[5], data[6]]) as usize;
287    let table_end = 7 + n * ENTRY_SIZE;
288    if data.len() < table_end {
289        return Err(DocBodyError::TruncatedData);
290    }
291
292    let mut table = Vec::with_capacity(n);
293    for i in 0..n {
294        let base = 7 + i * ENTRY_SIZE;
295        let key_len = u16::from_le_bytes([data[base], data[base + 1]]);
296        let val_offset = u32::from_le_bytes([
297            data[base + 2],
298            data[base + 3],
299            data[base + 4],
300            data[base + 5],
301        ]);
302        table.push((key_len, val_offset));
303    }
304
305    Ok((n, table))
306}
307
308/// Encode `fields` as a **dictionary-aware** (v2) document body container.
309///
310/// For each field `classify(name)` decides how the key is stored:
311///
312/// * `true`  — the key is *common*: it is interned into `dict` (appending a new
313///   id transactionally if it is not already present) and the body stores the
314///   compact key-id.
315/// * `false` — the key is *rare/unique*: it is stored **inline** in the body
316///   and never enters `dict`, so a heterogeneous collection cannot bloat the
317///   shared catalogue.
318///
319/// Decode with [`decode_with_dictionary`] using the (post-encode) dictionary.
320pub fn encode_with_dictionary(
321    fields: &[(&str, &Value)],
322    dict: &mut KeyDictionary,
323    classify: impl Fn(&str) -> bool,
324    out: &mut Vec<u8>,
325) -> Result<(), DocBodyError> {
326    let n = fields.len();
327    if n > u16::MAX as usize {
328        return Err(DocBodyError::FieldLimitExceeded);
329    }
330
331    // Inline key bytes (only for rare keys) and value bytes, plus per-field
332    // table data, are staged so absolute offsets can be computed up front.
333    let mut key_buf: Vec<u8> = Vec::new();
334    let mut val_buf: Vec<u8> = Vec::new();
335    let mut kinds: Vec<u8> = Vec::with_capacity(n);
336    let mut refs: Vec<u32> = Vec::with_capacity(n);
337    let mut val_starts: Vec<u32> = Vec::with_capacity(n);
338
339    for (key, value) in fields {
340        if classify(key) {
341            let id = dict.intern(key);
342            kinds.push(KEY_KIND_DICT);
343            refs.push(id);
344        } else {
345            let kb = key.as_bytes();
346            if kb.len() > u16::MAX as usize {
347                return Err(DocBodyError::FieldLimitExceeded);
348            }
349            kinds.push(KEY_KIND_INLINE);
350            refs.push(kb.len() as u32);
351            key_buf.extend_from_slice(kb);
352        }
353        val_starts.push(val_buf.len() as u32); // relative for now
354        value_codec::encode(value, &mut val_buf);
355    }
356
357    // Absolute offset of the values section within the final container:
358    //   magic(4) + version(1) + num_fields(2) + table(n * DICT_ENTRY_SIZE) + inline keys
359    let vals_abs_start = 4 + 1 + 2 + n * DICT_ENTRY_SIZE + key_buf.len();
360
361    out.extend_from_slice(MAGIC);
362    out.push(VERSION_DICT);
363    out.extend_from_slice(&(n as u16).to_le_bytes());
364
365    for i in 0..n {
366        let abs_val_offset = vals_abs_start + val_starts[i] as usize;
367        out.push(kinds[i]);
368        out.extend_from_slice(&refs[i].to_le_bytes());
369        out.extend_from_slice(&(abs_val_offset as u32).to_le_bytes());
370    }
371
372    out.extend_from_slice(&key_buf);
373    out.extend_from_slice(&val_buf);
374
375    Ok(())
376}
377
378/// Decode all fields from a **dictionary-aware** (v2) document body container.
379///
380/// `dict` must be the per-collection dictionary that was used (and possibly
381/// extended) during [`encode_with_dictionary`]; dictionary key-ids are resolved
382/// back to field names through it, while inline keys are read straight from the
383/// body.  Fields are returned in encode order.
384pub fn decode_with_dictionary(
385    data: &[u8],
386    dict: &KeyDictionary,
387) -> Result<Vec<(String, Value)>, DocBodyError> {
388    let (n, table) = parse_dict_header(data)?;
389    let header_end = 7 + n * DICT_ENTRY_SIZE; // where the inline-keys section begins
390    let mut key_cursor = header_end;
391    let mut result = Vec::with_capacity(n);
392
393    for &(kind, key_ref, val_offset) in &table {
394        let key = match kind {
395            KEY_KIND_DICT => dict
396                .name_of(key_ref)
397                .ok_or(DocBodyError::UnknownKeyId(key_ref))?
398                .to_string(),
399            KEY_KIND_INLINE => {
400                let key_end = key_cursor + key_ref as usize;
401                if key_end > data.len() {
402                    return Err(DocBodyError::OffsetOutOfBounds);
403                }
404                let name = std::str::from_utf8(&data[key_cursor..key_end])
405                    .map_err(|_| DocBodyError::InvalidFieldName)?
406                    .to_string();
407                key_cursor = key_end;
408                name
409            }
410            other => return Err(DocBodyError::BadKeyKind(other)),
411        };
412
413        let value = decode_value_at_offset(data, val_offset)?;
414        result.push((key, value));
415    }
416
417    Ok(result)
418}
419
420/// Parse a **v2** container header, returning `(num_fields, table)` where each
421/// table entry is `(key_kind, key_ref, val_offset)`.
422///
423/// As with [`parse_header`], inline key names and values are not validated
424/// here — that is deferred to [`decode_with_dictionary`].
425pub fn parse_dict_header(data: &[u8]) -> Result<(usize, Vec<DictEntry>), DocBodyError> {
426    if data.len() < 7 {
427        return Err(DocBodyError::TruncatedData);
428    }
429    if &data[0..4] != MAGIC.as_slice() {
430        return Err(DocBodyError::BadMagic);
431    }
432    if data[4] != VERSION_DICT {
433        return Err(DocBodyError::UnsupportedVersion(data[4]));
434    }
435
436    let n = u16::from_le_bytes([data[5], data[6]]) as usize;
437    let table_end = 7 + n * DICT_ENTRY_SIZE;
438    if data.len() < table_end {
439        return Err(DocBodyError::TruncatedData);
440    }
441
442    let mut table = Vec::with_capacity(n);
443    for i in 0..n {
444        let base = 7 + i * DICT_ENTRY_SIZE;
445        let kind = data[base];
446        let key_ref = u32::from_le_bytes([
447            data[base + 1],
448            data[base + 2],
449            data[base + 3],
450            data[base + 4],
451        ]);
452        let val_offset = u32::from_le_bytes([
453            data[base + 5],
454            data[base + 6],
455            data[base + 7],
456            data[base + 8],
457        ]);
458        table.push((kind, key_ref, val_offset));
459    }
460
461    Ok((n, table))
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::types::Value;
468    use std::net::{IpAddr, Ipv4Addr};
469
470    fn round_trip(fields: &[(&str, Value)]) -> Vec<(String, Value)> {
471        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
472        let mut buf = Vec::new();
473        encode(&refs, &mut buf).expect("encode");
474        decode(&buf).expect("decode")
475    }
476
477    #[test]
478    fn empty_document_round_trips() {
479        let got = round_trip(&[]);
480        assert!(got.is_empty());
481    }
482
483    #[test]
484    fn single_field_round_trip() {
485        let fields = [("name", Value::text("Alice"))];
486        let got = round_trip(&fields);
487        assert_eq!(got.len(), 1);
488        assert_eq!(got[0].0, "name");
489        assert_eq!(got[0].1, Value::text("Alice"));
490    }
491
492    #[test]
493    fn multi_field_round_trip() {
494        let fields = [
495            ("id", Value::Integer(42)),
496            ("email", Value::Email("a@example.com".to_string())),
497            ("active", Value::Boolean(true)),
498            ("score", Value::Float(9.5)),
499        ];
500        let got = round_trip(&fields);
501        assert_eq!(got.len(), 4);
502        for (i, (k, v)) in fields.iter().enumerate() {
503            assert_eq!(got[i].0, *k);
504            assert_eq!(got[i].1, *v);
505        }
506    }
507
508    /// Every rich semantic type must survive the round-trip unchanged.
509    #[test]
510    fn rich_semantic_types_round_trip() {
511        let fields = [
512            ("email", Value::Email("user@example.com".to_string())),
513            ("ipv4", Value::Ipv4(0x7f000001)),
514            ("subnet", Value::Subnet(0x0a000000, 0xff000000)),
515            ("color", Value::Color([0xDE, 0xAD, 0xBE])),
516            ("phone", Value::Phone(5511999000000)),
517            ("semver", Value::Semver(1_002_003)),
518            ("uuid", Value::Uuid([0xAB; 16])),
519            (
520                "money",
521                Value::Money {
522                    asset_code: "USD".to_string(),
523                    minor_units: 9999,
524                    scale: 2,
525                },
526            ),
527            ("geo", Value::GeoPoint(-23_550_520, -46_633_308)),
528            (
529                "ip_mixed",
530                Value::IpAddr(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))),
531            ),
532            ("url", Value::Url("https://reddb.io".to_string())),
533            ("color_alpha", Value::ColorAlpha([1, 2, 3, 255])),
534            ("lang", Value::Lang2(*b"en")),
535            ("country", Value::Country3(*b"USA")),
536        ];
537        let got = round_trip(&fields);
538        assert_eq!(got.len(), fields.len());
539        for (i, (k, v)) in fields.iter().enumerate() {
540            assert_eq!(got[i].0, *k, "key mismatch at {i}");
541            assert_eq!(got[i].1, *v, "value mismatch for field {k}");
542        }
543    }
544
545    /// A single field can be read by name without decoding the rest.
546    #[test]
547    fn read_field_by_name_skips_other_fields() {
548        let fields = [
549            ("a", Value::Integer(1)),
550            ("b", Value::text("hello")),
551            ("c", Value::Boolean(false)),
552        ];
553        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
554        let mut buf = Vec::new();
555        encode(&refs, &mut buf).expect("encode");
556
557        let v = read_field_by_name(&buf, "b")
558            .expect("lookup")
559            .expect("found");
560        assert_eq!(v, Value::text("hello"));
561
562        let missing = read_field_by_name(&buf, "z").expect("lookup");
563        assert!(missing.is_none());
564    }
565
566    /// `field_names` lists the keys in encode order without decoding values.
567    #[test]
568    fn field_names_lists_keys_in_encode_order() {
569        let fields = [
570            ("name", Value::text("Alice")),
571            ("score", Value::Integer(30)),
572            ("active", Value::Boolean(true)),
573        ];
574        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
575        let mut buf = Vec::new();
576        encode(&refs, &mut buf).expect("encode");
577
578        let names = field_names(&buf).expect("field_names");
579        assert_eq!(names, vec!["name", "score", "active"]);
580
581        assert!(field_names(&[]).is_err());
582    }
583
584    /// decode_value_at_offset provides direct O(1) access given a known offset.
585    #[test]
586    fn decode_value_at_offset_matches_full_decode() {
587        let fields = [("x", Value::Integer(100)), ("y", Value::Float(1.5))];
588        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
589        let mut buf = Vec::new();
590        encode(&refs, &mut buf).expect("encode");
591
592        let (n, table) = parse_header(&buf).expect("parse_header");
593        assert_eq!(n, 2);
594
595        for (i, (k, v)) in fields.iter().enumerate() {
596            let (_key_len, val_offset) = table[i];
597            let got = decode_value_at_offset(&buf, val_offset).expect("decode_at_offset");
598            assert_eq!(got, *v, "field {k} mismatch");
599        }
600    }
601
602    #[test]
603    fn rejects_bad_magic() {
604        let mut buf = vec![b'X', b'X', b'X', b'X', VERSION, 0, 0];
605        assert_eq!(decode(&buf), Err(DocBodyError::BadMagic));
606        buf[0..4].copy_from_slice(MAGIC);
607        buf[4] = 0x99;
608        assert_eq!(decode(&buf), Err(DocBodyError::UnsupportedVersion(0x99)));
609    }
610
611    #[test]
612    fn rejects_truncated_buffer() {
613        assert_eq!(decode(&[]), Err(DocBodyError::TruncatedData));
614        assert_eq!(
615            decode(&[b'R', b'D', b'O', b'C', 1, 0]),
616            Err(DocBodyError::TruncatedData)
617        );
618    }
619
620    #[test]
621    fn null_values_round_trip() {
622        let fields = [("nothing", Value::Null), ("something", Value::Integer(7))];
623        let got = round_trip(&fields);
624        assert_eq!(got[0].1, Value::Null);
625        assert_eq!(got[1].1, Value::Integer(7));
626    }
627
628    #[test]
629    fn array_value_round_trip() {
630        let fields = [(
631            "tags",
632            Value::Array(vec![Value::text("a"), Value::text("b"), Value::Integer(3)]),
633        )];
634        let got = round_trip(&fields);
635        assert_eq!(
636            got[0].1,
637            Value::Array(vec![Value::text("a"), Value::text("b"), Value::Integer(3)])
638        );
639    }
640
641    /// Verify the offset table points to the correct byte positions.
642    #[test]
643    fn offset_table_offsets_are_valid() {
644        let fields = [("k", Value::Boolean(true))];
645        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
646        let mut buf = Vec::new();
647        encode(&refs, &mut buf).expect("encode");
648
649        let (n, table) = parse_header(&buf).expect("header");
650        assert_eq!(n, 1);
651        let (_klen, val_offset) = table[0];
652        // The value bytes at val_offset must decode to Boolean(true)
653        let (v, _) = value_codec::decode(&buf[val_offset as usize..]).expect("decode at offset");
654        assert_eq!(v, Value::Boolean(true));
655    }
656
657    // ---- Dictionary-aware (v2) container ----------------------------------
658
659    /// Classify every key as common — exercises the homogeneous-collection path.
660    fn all_common(_: &str) -> bool {
661        true
662    }
663
664    /// Common field names are interned in the dictionary and the body stores
665    /// compact key-ids, not the key strings.
666    #[test]
667    fn common_keys_are_interned_as_key_ids() {
668        let fields = [
669            ("id", Value::Integer(1)),
670            ("name", Value::text("Alice")),
671            ("active", Value::Boolean(true)),
672        ];
673        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
674        let mut dict = KeyDictionary::new();
675        let mut buf = Vec::new();
676        encode_with_dictionary(&refs, &mut dict, all_common, &mut buf).expect("encode");
677
678        // All three keys entered the dictionary with dense ids.
679        assert_eq!(dict.len(), 3);
680        assert_eq!(dict.id_of("id"), Some(0));
681        assert_eq!(dict.id_of("name"), Some(1));
682        assert_eq!(dict.id_of("active"), Some(2));
683
684        // Every table entry is a dictionary reference, no inline keys.
685        let (n, table) = parse_dict_header(&buf).expect("header");
686        assert_eq!(n, 3);
687        for (kind, _key_ref, _off) in &table {
688            assert_eq!(*kind, KEY_KIND_DICT);
689        }
690        // The string "name" is not repeated in the body bytes — only its id is.
691        assert!(!buf.windows(4).any(|w| w == b"name"));
692    }
693
694    /// Encoding a second document with a new common key appends to the existing
695    /// dictionary transactionally (alongside the write), reusing prior ids.
696    #[test]
697    fn new_common_key_appends_to_dictionary() {
698        let mut dict = KeyDictionary::new();
699
700        let doc1 = [("id", Value::Integer(1)), ("name", Value::text("Alice"))];
701        let r1: Vec<(&str, &Value)> = doc1.iter().map(|(k, v)| (*k, v)).collect();
702        let mut b1 = Vec::new();
703        encode_with_dictionary(&r1, &mut dict, all_common, &mut b1).expect("encode 1");
704        assert_eq!(dict.len(), 2);
705
706        // Second document reuses id/name and introduces a brand-new common key.
707        let doc2 = [
708            ("id", Value::Integer(2)),
709            ("name", Value::text("Bob")),
710            ("email", Value::Email("bob@example.com".to_string())),
711        ];
712        let r2: Vec<(&str, &Value)> = doc2.iter().map(|(k, v)| (*k, v)).collect();
713        let mut b2 = Vec::new();
714        encode_with_dictionary(&r2, &mut dict, all_common, &mut b2).expect("encode 2");
715
716        // Only the new key appended; existing ids are stable.
717        assert_eq!(dict.len(), 3);
718        assert_eq!(dict.id_of("id"), Some(0));
719        assert_eq!(dict.id_of("name"), Some(1));
720        assert_eq!(dict.id_of("email"), Some(2));
721
722        // Both documents decode losslessly against the shared dictionary.
723        let g1 = decode_with_dictionary(&b1, &dict).expect("decode 1");
724        assert_eq!(g1[1], ("name".to_string(), Value::text("Alice")));
725        let g2 = decode_with_dictionary(&b2, &dict).expect("decode 2");
726        assert_eq!(
727            g2[2],
728            (
729                "email".to_string(),
730                Value::Email("bob@example.com".to_string())
731            )
732        );
733    }
734
735    /// A rare/unique key is stored inline and never enters the dictionary.
736    #[test]
737    fn rare_key_stays_inline_and_out_of_dictionary() {
738        // "id"/"name" are common; anything else is rare.
739        let common = |k: &str| k == "id" || k == "name";
740        let fields = [
741            ("id", Value::Integer(7)),
742            ("name", Value::text("Carol")),
743            ("x9f3_one_off_attr", Value::text("rare-value")),
744        ];
745        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
746        let mut dict = KeyDictionary::new();
747        let mut buf = Vec::new();
748        encode_with_dictionary(&refs, &mut dict, common, &mut buf).expect("encode");
749
750        // The rare key never entered the catalogue.
751        assert_eq!(dict.len(), 2);
752        assert_eq!(dict.id_of("x9f3_one_off_attr"), None);
753
754        // Its kind tag is inline and its name bytes live in the body.
755        let (_n, table) = parse_dict_header(&buf).expect("header");
756        assert_eq!(table[0].0, KEY_KIND_DICT);
757        assert_eq!(table[1].0, KEY_KIND_DICT);
758        assert_eq!(table[2].0, KEY_KIND_INLINE);
759        assert!(buf
760            .windows("x9f3_one_off_attr".len())
761            .any(|w| w == b"x9f3_one_off_attr"));
762
763        // And it round-trips losslessly.
764        let got = decode_with_dictionary(&buf, &dict).expect("decode");
765        assert_eq!(
766            got[2],
767            ("x9f3_one_off_attr".to_string(), Value::text("rare-value"))
768        );
769    }
770
771    /// Round-trip mixing dictionary keys and inline keys is lossless, including
772    /// rich semantic types and after persisting/reloading the dictionary.
773    #[test]
774    fn mixed_dictionary_and_inline_round_trip() {
775        let common = |k: &str| matches!(k, "id" | "email" | "active");
776        let fields = [
777            ("id", Value::Integer(42)),
778            ("email", Value::Email("a@example.com".to_string())),
779            ("active", Value::Boolean(true)),
780            ("one_off_color", Value::Color([0xDE, 0xAD, 0xBE])),
781            ("rare_geo", Value::GeoPoint(-23_550_520, -46_633_308)),
782        ];
783        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
784        let mut dict = KeyDictionary::new();
785        let mut buf = Vec::new();
786        encode_with_dictionary(&refs, &mut dict, common, &mut buf).expect("encode");
787
788        // Persist and reload the dictionary, then decode against the reloaded copy.
789        let mut dict_bytes = Vec::new();
790        dict.encode(&mut dict_bytes);
791        let reloaded = KeyDictionary::decode(&dict_bytes).expect("reload dict");
792
793        let got = decode_with_dictionary(&buf, &reloaded).expect("decode");
794        assert_eq!(got.len(), fields.len());
795        for (i, (k, v)) in fields.iter().enumerate() {
796            assert_eq!(got[i].0, *k, "key mismatch at {i}");
797            assert_eq!(got[i].1, *v, "value mismatch for field {k}");
798        }
799    }
800
801    #[test]
802    fn empty_dictionary_document_round_trips() {
803        let mut dict = KeyDictionary::new();
804        let mut buf = Vec::new();
805        encode_with_dictionary(&[], &mut dict, all_common, &mut buf).expect("encode");
806        let got = decode_with_dictionary(&buf, &dict).expect("decode");
807        assert!(got.is_empty());
808        assert!(dict.is_empty());
809    }
810
811    /// A v2 body decoded against a dictionary that lacks the id is rejected,
812    /// not silently mis-decoded.
813    #[test]
814    fn unknown_key_id_is_rejected() {
815        let fields = [("only_key", Value::Integer(1))];
816        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
817        let mut dict = KeyDictionary::new();
818        let mut buf = Vec::new();
819        encode_with_dictionary(&refs, &mut dict, all_common, &mut buf).expect("encode");
820
821        let empty = KeyDictionary::new();
822        assert_eq!(
823            decode_with_dictionary(&buf, &empty),
824            Err(DocBodyError::UnknownKeyId(0))
825        );
826    }
827
828    /// v1 and v2 containers are distinguished by their version byte; decoding a
829    /// v2 body with the v1 path (and vice versa) fails on the version check.
830    #[test]
831    fn v1_and_v2_versions_do_not_alias() {
832        let fields = [("k", Value::Integer(1))];
833        let refs: Vec<(&str, &Value)> = fields.iter().map(|(k, v)| (*k, v)).collect();
834
835        let mut v1 = Vec::new();
836        encode(&refs, &mut v1).expect("v1");
837        assert_eq!(v1[4], VERSION);
838
839        let mut dict = KeyDictionary::new();
840        let mut v2 = Vec::new();
841        encode_with_dictionary(&refs, &mut dict, all_common, &mut v2).expect("v2");
842        assert_eq!(v2[4], VERSION_DICT);
843
844        // Cross-decoding hits the version guard.
845        assert_eq!(
846            decode(&v2),
847            Err(DocBodyError::UnsupportedVersion(VERSION_DICT))
848        );
849        assert_eq!(
850            parse_dict_header(&v1),
851            Err(DocBodyError::UnsupportedVersion(VERSION))
852        );
853    }
854}