Skip to main content

reddb_server/
document_body.rs

1//! Native binary document-body container wiring (PRD-1398, ADR-0063).
2//!
3//! DOCUMENT collections keep the canonical document under a `body` field.
4//! Behind the `storage.binary_document_body` flag, writes serialise that body into the
5//! native binary container ([`reddb_types::document_body_codec`]) instead of
6//! plain UTF-8 JSON. The container is self-describing — it starts with the
7//! `RDOC` magic — so every read path decodes it back to JSON transparently,
8//! regardless of the flag, keeping the wire/clients on JSON (no driver change).
9//!
10//! The binary form lives only inside the stored `Value::Json` bytes; the rest
11//! of the engine continues to see a JSON body in memory. This is the
12//! binary write + binary-to-JSON read path; bare field filters/projections
13//! offset-read from the body after the production cutover.
14
15use reddb_types::document_body_codec;
16
17use crate::application::entity::json_to_storage_value;
18use crate::json::{to_vec as json_to_vec, Map, Value as JsonValue};
19use crate::{RedDBError, RedDBResult};
20use reddb_types::Value;
21
22/// True when `bytes` begin with the document-body container magic.
23///
24/// A legacy UTF-8 JSON document body is an object and starts with `{`, so this
25/// magic check never collides with a JSON body.
26pub(crate) fn is_binary_container(bytes: &[u8]) -> bool {
27    bytes.starts_with(document_body_codec::MAGIC)
28}
29
30/// Decode a binary document-body container back to its JSON object.
31///
32/// Returns `None` when `bytes` is not a container — so callers fall back to
33/// their existing JSON parse — or when a container fails to decode.
34pub(crate) use reddb_types::document_body_codec::decode_container_to_json;
35
36/// Offset-read a single top-level field from a stored document body.
37///
38/// Returns the field's storage [`Value`] decoded directly from the binary
39/// container by offset ([`document_body_codec::read_field_by_name`]) — no
40/// other field is touched. Returns `None` when `bytes` is not a binary
41/// container or the field is absent.
42///
43/// This is the read seam that makes the body the single source of truth: a
44/// `WHERE`/projection on a top-level field routes here to read it straight
45/// from the body.
46pub(crate) fn read_body_field(bytes: &[u8], name: &str) -> Option<Value> {
47    if !is_binary_container(bytes) {
48        return None;
49    }
50    document_body_codec::read_field_by_name(bytes, name)
51        .ok()
52        .flatten()
53}
54
55/// Decode every top-level field of a binary document body as storage values.
56///
57/// Returns `None` when `bytes` is not a binary container. Used to expand a
58/// `SELECT *` over a single-source document back into its top-level fields.
59pub(crate) fn body_fields(bytes: &[u8]) -> Option<Vec<(String, Value)>> {
60    if !is_binary_container(bytes) {
61        return None;
62    }
63    document_body_codec::decode(bytes).ok()
64}
65
66/// List the top-level field names of a binary document body (offset-read of
67/// the keys section only). Returns `None` for non-container bytes.
68pub(crate) fn container_field_names(bytes: &[u8]) -> Option<Vec<String>> {
69    if !is_binary_container(bytes) {
70        return None;
71    }
72    document_body_codec::field_names(bytes).ok()
73}
74
75/// Serialise a document body for storage in the `body` field.
76///
77/// With `binary` set and an object body, produce the native binary container;
78/// otherwise (or for a non-object body, which the container cannot represent)
79/// fall back to UTF-8 JSON bytes. The caller wraps the result in `Value::Json`.
80pub(crate) fn serialize_document_body(body: &JsonValue, binary: bool) -> RedDBResult<Vec<u8>> {
81    if binary {
82        if let JsonValue::Object(map) = body {
83            let typed: Vec<(String, Value)> = map
84                .iter()
85                .map(|(key, value)| Ok((key.clone(), json_to_storage_value(value)?)))
86                .collect::<RedDBResult<_>>()?;
87            let refs: Vec<(&str, &Value)> = typed
88                .iter()
89                .map(|(key, value)| (key.as_str(), value))
90                .collect();
91            let mut out = Vec::new();
92            document_body_codec::encode(&refs, &mut out).map_err(|err| {
93                RedDBError::Query(format!("failed to encode binary document body: {err}"))
94            })?;
95            return Ok(out);
96        }
97    }
98    json_to_vec(body)
99        .map_err(|err| RedDBError::Query(format!("failed to serialize document body: {err}")))
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    fn parse(text: &str) -> JsonValue {
107        crate::json::from_str(text).expect("valid JSON fixture")
108    }
109
110    fn body() -> JsonValue {
111        parse(
112            r#"{"name":"Alice","age":30,"email":"alice@example.com",
113               "tags":["admin","ops"],"profile":{"city":"SP","active":true}}"#,
114        )
115    }
116
117    #[test]
118    fn binary_off_emits_plain_json_bytes() {
119        let bytes = serialize_document_body(&body(), false).expect("serialize");
120        assert!(!is_binary_container(&bytes), "flag off must stay JSON");
121        assert_eq!(bytes.first(), Some(&b'{'));
122        assert!(decode_container_to_json(&bytes).is_none());
123    }
124
125    #[test]
126    fn binary_on_emits_container_that_decodes_to_equal_json() {
127        let original = body();
128        let bytes = serialize_document_body(&original, true).expect("serialize");
129        assert!(is_binary_container(&bytes), "flag on must produce RDOC");
130        let decoded = decode_container_to_json(&bytes).expect("decode");
131        assert_eq!(
132            decoded, original,
133            "binary body must round-trip to equal JSON"
134        );
135    }
136
137    #[test]
138    fn high_precision_decimal_body_field_round_trips_exactly() {
139        let original = parse(r#"{"amount":3.14159265358979323846,"big":18446744073709551616}"#);
140        let bytes = serialize_document_body(&original, true).expect("serialize");
141        let decoded = decode_container_to_json(&bytes).expect("decode");
142        let expected = parse(
143            r#"{"amount":{"$decimal":"3.14159265358979323846"},"big":{"$decimal":"18446744073709551616"}}"#,
144        );
145        assert_eq!(decoded, expected);
146        assert_eq!(
147            read_body_field(&bytes, "amount"),
148            Some(Value::DecimalText("3.14159265358979323846".to_string()))
149        );
150        assert_eq!(
151            decoded.to_string_compact(),
152            r#"{"amount":{"$decimal":"3.14159265358979323846"},"big":{"$decimal":"18446744073709551616"}}"#
153        );
154    }
155
156    #[test]
157    fn non_object_body_falls_back_to_json_even_with_binary_on() {
158        let scalar = JsonValue::String("just-a-string".to_string());
159        let bytes = serialize_document_body(&scalar, true).expect("serialize");
160        assert!(!is_binary_container(&bytes));
161        assert_eq!(
162            json_to_vec(&scalar).unwrap(),
163            bytes,
164            "non-object body must serialise as plain JSON"
165        );
166    }
167
168    #[test]
169    fn rich_semantic_string_types_survive_round_trip() {
170        // On the JSON wire these are strings; the container must round-trip the
171        // exact JSON the client sent (Email/Ipv4/Subnet/Color string forms).
172        let original = parse(
173            r##"{"email":"user@example.com","ipv4":"127.0.0.1",
174               "subnet":"10.0.0.0/8","color":"#DEADBE","url":"https://reddb.io"}"##,
175        );
176        let bytes = serialize_document_body(&original, true).expect("serialize");
177        let decoded = decode_container_to_json(&bytes).expect("decode");
178        assert_eq!(decoded, original);
179    }
180
181    #[test]
182    fn read_body_field_offset_reads_from_binary_body() {
183        let bytes = serialize_document_body(&body(), true).expect("serialize");
184        assert_eq!(read_body_field(&bytes, "name"), Some(Value::text("Alice")));
185        assert_eq!(read_body_field(&bytes, "age"), Some(Value::Integer(30)));
186        assert_eq!(read_body_field(&bytes, "missing"), None);
187    }
188
189    #[test]
190    fn numeric_field_reads_do_not_allocate_offset_tables() {
191        use crate::storage::no_malloc_hot_paths::measure_allocations;
192        let fields: Vec<_> = (0..100)
193            .map(|index| (format!("field_{index}"), Value::Integer(index)))
194            .collect();
195        let references: Vec<_> = fields
196            .iter()
197            .map(|(key, value)| (key.as_str(), value))
198            .collect();
199        let mut bytes = Vec::new();
200        document_body_codec::encode(&references, &mut bytes).expect("numeric body");
201        let (_, allocations) = measure_allocations(|| {
202            for _ in 0..1000 {
203                for index in [0, 50, 99] {
204                    let name = match index {
205                        0 => "field_0",
206                        50 => "field_50",
207                        _ => "field_99",
208                    };
209                    assert_eq!(read_body_field(&bytes, name), Some(Value::Integer(index)));
210                }
211            }
212        });
213        assert_eq!(allocations.allocs, 0, "{allocations:?}");
214    }
215
216    #[test]
217    fn body_field_helpers_ignore_plain_json_bodies() {
218        let bytes = serialize_document_body(&body(), false).expect("serialize");
219        assert_eq!(read_body_field(&bytes, "name"), None);
220        assert_eq!(body_fields(&bytes), None);
221        assert_eq!(container_field_names(&bytes), None);
222    }
223
224    #[test]
225    fn body_fields_and_names_cover_top_level_keys() {
226        let bytes = serialize_document_body(&body(), true).expect("serialize");
227        let names = container_field_names(&bytes).expect("names");
228        for key in ["name", "age", "email", "tags", "profile"] {
229            assert!(names.contains(&key.to_string()), "missing {key}");
230        }
231        let fields = body_fields(&bytes).expect("fields");
232        assert_eq!(fields.len(), names.len());
233    }
234
235    #[test]
236    fn empty_object_round_trips() {
237        let original = parse("{}");
238        let bytes = serialize_document_body(&original, true).expect("serialize");
239        assert!(is_binary_container(&bytes));
240        assert_eq!(decode_container_to_json(&bytes), Some(original));
241    }
242}