Skip to main content

laminar_connectors/schema/json/
jsonb.rs

1//! JSONB binary format for O(log n) field access on Ring 0.
2//!
3//! The JSONB format is a compact binary encoding of JSON values with
4//! pre-computed byte offsets. Object keys are sorted alphabetically,
5//! enabling binary-search field lookups in <100ns for typical objects.
6//!
7//! # Type Tags
8//!
9//! | Tag | Type | Data |
10//! |-----|------|------|
11//! | 0x00 | Null | (none) |
12//! | 0x01 | Boolean false | (none) |
13//! | 0x02 | Boolean true | (none) |
14//! | 0x03 | Int64 | 8 bytes LE |
15//! | 0x04 | Float64 | 8 bytes IEEE 754 LE |
16//! | 0x05 | String | 4-byte LE length + UTF-8 bytes |
17//! | 0x06 | Array | 4-byte count + offset table + elements |
18//! | 0x07 | Object | 4-byte count + offset table + key-value data |
19
20/// Re-export canonical JSONB binary format type tags from `laminar-core`.
21pub use laminar_core::serialization::jsonb_tags as tags;
22
23/// Encodes `serde_json::Value` into JSONB binary format.
24///
25/// Used in Ring 1 during JSON decode to pre-compute the binary
26/// representation that Ring 0 accesses via [`JsonbAccessor`].
27#[derive(Debug)]
28pub struct JsonbEncoder {
29    buf: Vec<u8>,
30}
31
32impl JsonbEncoder {
33    /// Creates a new encoder with a default 4 KiB buffer.
34    #[must_use]
35    pub fn new() -> Self {
36        Self {
37            buf: Vec::with_capacity(4096),
38        }
39    }
40
41    /// Encodes a JSON value into JSONB binary format, returning the bytes.
42    pub fn encode(&mut self, value: &serde_json::Value) -> Vec<u8> {
43        self.buf.clear();
44        self.encode_value(value);
45        self.buf.clone()
46    }
47
48    #[allow(clippy::cast_possible_truncation)]
49    fn encode_value(&mut self, value: &serde_json::Value) {
50        match value {
51            serde_json::Value::Null => self.buf.push(tags::NULL),
52            serde_json::Value::Bool(false) => self.buf.push(tags::BOOL_FALSE),
53            serde_json::Value::Bool(true) => self.buf.push(tags::BOOL_TRUE),
54            serde_json::Value::Number(n) => {
55                if let Some(i) = n.as_i64() {
56                    self.buf.push(tags::INT64);
57                    self.buf.extend_from_slice(&i.to_le_bytes());
58                } else if let Some(f) = n.as_f64() {
59                    self.buf.push(tags::FLOAT64);
60                    self.buf.extend_from_slice(&f.to_le_bytes());
61                }
62            }
63            serde_json::Value::String(s) => {
64                self.buf.push(tags::STRING);
65                self.buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
66                self.buf.extend_from_slice(s.as_bytes());
67            }
68            serde_json::Value::Array(arr) => {
69                self.buf.push(tags::ARRAY);
70                self.buf
71                    .extend_from_slice(&(arr.len() as u32).to_le_bytes());
72                // Reserve space for offset table.
73                let offset_table_pos = self.buf.len();
74                self.buf.resize(self.buf.len() + arr.len() * 4, 0);
75                let data_start = self.buf.len();
76                for (i, elem) in arr.iter().enumerate() {
77                    let elem_offset = (self.buf.len() - data_start) as u32;
78                    let entry_pos = offset_table_pos + i * 4;
79                    self.buf[entry_pos..entry_pos + 4].copy_from_slice(&elem_offset.to_le_bytes());
80                    self.encode_value(elem);
81                }
82            }
83            serde_json::Value::Object(obj) => {
84                self.buf.push(tags::OBJECT);
85                // Sort keys for binary search.
86                let mut keys: Vec<&String> = obj.keys().collect();
87                keys.sort();
88                self.buf
89                    .extend_from_slice(&(keys.len() as u32).to_le_bytes());
90                // Reserve space for offset table (key_off + val_off per field).
91                let offset_table_pos = self.buf.len();
92                self.buf.resize(self.buf.len() + keys.len() * 8, 0);
93                let data_start = self.buf.len();
94
95                for (i, key) in keys.iter().enumerate() {
96                    // Write key offset.
97                    let key_offset = (self.buf.len() - data_start) as u32;
98                    let entry_pos = offset_table_pos + i * 8;
99                    self.buf[entry_pos..entry_pos + 4].copy_from_slice(&key_offset.to_le_bytes());
100                    // Write key (u16 length + UTF-8 bytes).
101                    self.buf
102                        .extend_from_slice(&(key.len() as u16).to_le_bytes());
103                    self.buf.extend_from_slice(key.as_bytes());
104                    // Write value offset.
105                    let val_offset = (self.buf.len() - data_start) as u32;
106                    self.buf[entry_pos + 4..entry_pos + 8]
107                        .copy_from_slice(&val_offset.to_le_bytes());
108                    // Write value.
109                    self.encode_value(&obj[*key]);
110                }
111            }
112        }
113    }
114}
115
116impl Default for JsonbEncoder {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122/// Zero-allocation JSONB accessor for Ring 0 hot-path field lookups.
123///
124/// All operations return byte slices into the original JSONB binary
125/// buffer — no heap allocation occurs.
126pub struct JsonbAccessor;
127
128impl JsonbAccessor {
129    /// Access a field by name in a JSONB object.
130    ///
131    /// Returns a byte slice pointing to the field's JSONB value,
132    /// or `None` if the field does not exist or the value is not an object.
133    ///
134    /// Performance: O(log n) binary search on sorted keys.
135    #[inline]
136    #[must_use]
137    pub fn get_field<'a>(jsonb: &'a [u8], field_name: &str) -> Option<&'a [u8]> {
138        if jsonb.is_empty() || jsonb[0] != tags::OBJECT {
139            return None;
140        }
141
142        let field_count = u32::from_le_bytes(jsonb.get(1..5)?.try_into().ok()?) as usize;
143        if field_count == 0 {
144            return None;
145        }
146
147        let offset_table_start = 5;
148        let offset_table_end = offset_table_start + field_count * 8;
149        let data_start = offset_table_end;
150
151        // Binary search on sorted keys.
152        let mut lo = 0usize;
153        let mut hi = field_count;
154        while lo < hi {
155            let mid = lo + (hi - lo) / 2;
156            let entry_offset = offset_table_start + mid * 8;
157            let key_off =
158                u32::from_le_bytes(jsonb.get(entry_offset..entry_offset + 4)?.try_into().ok()?)
159                    as usize;
160
161            let key_abs = data_start + key_off;
162            let key_len =
163                u16::from_le_bytes(jsonb.get(key_abs..key_abs + 2)?.try_into().ok()?) as usize;
164            let key_bytes = jsonb.get(key_abs + 2..key_abs + 2 + key_len)?;
165            let key_str = std::str::from_utf8(key_bytes).ok()?;
166
167            match key_str.cmp(field_name) {
168                std::cmp::Ordering::Equal => {
169                    let val_off = u32::from_le_bytes(
170                        jsonb
171                            .get(entry_offset + 4..entry_offset + 8)?
172                            .try_into()
173                            .ok()?,
174                    ) as usize;
175                    let val_abs = data_start + val_off;
176                    return jsonb.get(val_abs..);
177                }
178                std::cmp::Ordering::Less => lo = mid + 1,
179                std::cmp::Ordering::Greater => hi = mid,
180            }
181        }
182        None
183    }
184
185    /// Returns `true` if the JSONB value is null (tag 0x00).
186    #[inline]
187    #[must_use]
188    pub fn is_null(jsonb_value: &[u8]) -> bool {
189        !jsonb_value.is_empty() && jsonb_value[0] == tags::NULL
190    }
191
192    /// Extract a boolean from a JSONB value slice.
193    #[inline]
194    #[must_use]
195    pub fn as_bool(jsonb_value: &[u8]) -> Option<bool> {
196        match *jsonb_value.first()? {
197            tags::BOOL_FALSE => Some(false),
198            tags::BOOL_TRUE => Some(true),
199            _ => None,
200        }
201    }
202
203    /// Extract an i64 from a JSONB value slice.
204    #[inline]
205    #[must_use]
206    pub fn as_i64(jsonb_value: &[u8]) -> Option<i64> {
207        if jsonb_value.first()? != &tags::INT64 {
208            return None;
209        }
210        Some(i64::from_le_bytes(jsonb_value.get(1..9)?.try_into().ok()?))
211    }
212
213    /// Extract an f64 from a JSONB value slice.
214    #[inline]
215    #[must_use]
216    pub fn as_f64(jsonb_value: &[u8]) -> Option<f64> {
217        if jsonb_value.first()? != &tags::FLOAT64 {
218            return None;
219        }
220        Some(f64::from_le_bytes(jsonb_value.get(1..9)?.try_into().ok()?))
221    }
222
223    /// Extract a string from a JSONB value slice.
224    #[inline]
225    #[must_use]
226    pub fn as_str(jsonb_value: &[u8]) -> Option<&str> {
227        if jsonb_value.first()? != &tags::STRING {
228            return None;
229        }
230        let len = u32::from_le_bytes(jsonb_value.get(1..5)?.try_into().ok()?) as usize;
231        std::str::from_utf8(jsonb_value.get(5..5 + len)?).ok()
232    }
233
234    /// Get the element count of a JSONB array.
235    #[inline]
236    #[must_use]
237    pub fn array_len(jsonb_value: &[u8]) -> Option<usize> {
238        if jsonb_value.first()? != &tags::ARRAY {
239            return None;
240        }
241        Some(u32::from_le_bytes(jsonb_value.get(1..5)?.try_into().ok()?) as usize)
242    }
243
244    /// Get a JSONB array element by index.
245    #[inline]
246    #[must_use]
247    pub fn array_get(jsonb_value: &[u8], index: usize) -> Option<&[u8]> {
248        if jsonb_value.first()? != &tags::ARRAY {
249            return None;
250        }
251        let count = u32::from_le_bytes(jsonb_value.get(1..5)?.try_into().ok()?) as usize;
252        if index >= count {
253            return None;
254        }
255        let offset_table_start = 5;
256        let data_start = offset_table_start + count * 4;
257        let entry_pos = offset_table_start + index * 4;
258        let elem_off =
259            u32::from_le_bytes(jsonb_value.get(entry_pos..entry_pos + 4)?.try_into().ok()?)
260                as usize;
261        jsonb_value.get(data_start + elem_off..)
262    }
263
264    /// Get the field count of a JSONB object.
265    #[inline]
266    #[must_use]
267    pub fn object_len(jsonb_value: &[u8]) -> Option<usize> {
268        if jsonb_value.first()? != &tags::OBJECT {
269            return None;
270        }
271        Some(u32::from_le_bytes(jsonb_value.get(1..5)?.try_into().ok()?) as usize)
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use serde_json::json;
279
280    #[test]
281    fn test_encode_null() {
282        let mut enc = JsonbEncoder::new();
283        let bytes = enc.encode(&json!(null));
284        assert_eq!(bytes, vec![tags::NULL]);
285    }
286
287    #[test]
288    fn test_encode_bool() {
289        let mut enc = JsonbEncoder::new();
290        assert_eq!(enc.encode(&json!(false)), vec![tags::BOOL_FALSE]);
291        assert_eq!(enc.encode(&json!(true)), vec![tags::BOOL_TRUE]);
292    }
293
294    #[test]
295    fn test_encode_int64() {
296        let mut enc = JsonbEncoder::new();
297        let bytes = enc.encode(&json!(42));
298        assert_eq!(bytes[0], tags::INT64);
299        let val = i64::from_le_bytes(bytes[1..9].try_into().unwrap());
300        assert_eq!(val, 42);
301    }
302
303    #[test]
304    fn test_encode_float64() {
305        let mut enc = JsonbEncoder::new();
306        let bytes = enc.encode(&json!(3.14));
307        assert_eq!(bytes[0], tags::FLOAT64);
308        let val = f64::from_le_bytes(bytes[1..9].try_into().unwrap());
309        assert!((val - 3.14).abs() < f64::EPSILON);
310    }
311
312    #[test]
313    fn test_encode_string() {
314        let mut enc = JsonbEncoder::new();
315        let bytes = enc.encode(&json!("hello"));
316        assert_eq!(bytes[0], tags::STRING);
317        let len = u32::from_le_bytes(bytes[1..5].try_into().unwrap()) as usize;
318        assert_eq!(len, 5);
319        assert_eq!(&bytes[5..10], b"hello");
320    }
321
322    #[test]
323    fn test_accessor_null() {
324        let mut enc = JsonbEncoder::new();
325        let bytes = enc.encode(&json!(null));
326        assert!(JsonbAccessor::is_null(&bytes));
327        assert!(JsonbAccessor::as_bool(&bytes).is_none());
328    }
329
330    #[test]
331    fn test_accessor_bool() {
332        let mut enc = JsonbEncoder::new();
333        assert_eq!(
334            JsonbAccessor::as_bool(&enc.encode(&json!(true))),
335            Some(true)
336        );
337        assert_eq!(
338            JsonbAccessor::as_bool(&enc.encode(&json!(false))),
339            Some(false)
340        );
341    }
342
343    #[test]
344    fn test_accessor_i64() {
345        let mut enc = JsonbEncoder::new();
346        let bytes = enc.encode(&json!(-99));
347        assert_eq!(JsonbAccessor::as_i64(&bytes), Some(-99));
348    }
349
350    #[test]
351    fn test_accessor_f64() {
352        let mut enc = JsonbEncoder::new();
353        let bytes = enc.encode(&json!(2.718));
354        let val = JsonbAccessor::as_f64(&bytes).unwrap();
355        assert!((val - 2.718).abs() < f64::EPSILON);
356    }
357
358    #[test]
359    fn test_accessor_str() {
360        let mut enc = JsonbEncoder::new();
361        let bytes = enc.encode(&json!("world"));
362        assert_eq!(JsonbAccessor::as_str(&bytes), Some("world"));
363    }
364
365    #[test]
366    fn test_object_field_access() {
367        let mut enc = JsonbEncoder::new();
368        let bytes = enc.encode(&json!({"name": "Alice", "age": 30, "active": true}));
369
370        // Fields are sorted: "active", "age", "name".
371        let name_val = JsonbAccessor::get_field(&bytes, "name").unwrap();
372        assert_eq!(JsonbAccessor::as_str(name_val), Some("Alice"));
373
374        let age_val = JsonbAccessor::get_field(&bytes, "age").unwrap();
375        assert_eq!(JsonbAccessor::as_i64(age_val), Some(30));
376
377        let active_val = JsonbAccessor::get_field(&bytes, "active").unwrap();
378        assert_eq!(JsonbAccessor::as_bool(active_val), Some(true));
379
380        // Non-existent field.
381        assert!(JsonbAccessor::get_field(&bytes, "missing").is_none());
382    }
383
384    #[test]
385    fn test_object_empty() {
386        let mut enc = JsonbEncoder::new();
387        let bytes = enc.encode(&json!({}));
388        assert_eq!(JsonbAccessor::object_len(&bytes), Some(0));
389        assert!(JsonbAccessor::get_field(&bytes, "any").is_none());
390    }
391
392    #[test]
393    fn test_array_access() {
394        let mut enc = JsonbEncoder::new();
395        let bytes = enc.encode(&json!([10, 20, 30]));
396
397        assert_eq!(JsonbAccessor::array_len(&bytes), Some(3));
398
399        let elem0 = JsonbAccessor::array_get(&bytes, 0).unwrap();
400        assert_eq!(JsonbAccessor::as_i64(elem0), Some(10));
401
402        let elem2 = JsonbAccessor::array_get(&bytes, 2).unwrap();
403        assert_eq!(JsonbAccessor::as_i64(elem2), Some(30));
404
405        assert!(JsonbAccessor::array_get(&bytes, 5).is_none());
406    }
407
408    #[test]
409    fn test_nested_object() {
410        let mut enc = JsonbEncoder::new();
411        let bytes = enc.encode(&json!({"outer": {"inner": 42}}));
412
413        let outer = JsonbAccessor::get_field(&bytes, "outer").unwrap();
414        let inner = JsonbAccessor::get_field(outer, "inner").unwrap();
415        assert_eq!(JsonbAccessor::as_i64(inner), Some(42));
416    }
417
418    #[test]
419    fn test_nested_array_in_object() {
420        let mut enc = JsonbEncoder::new();
421        let bytes = enc.encode(&json!({"items": [1, 2, 3]}));
422
423        let items = JsonbAccessor::get_field(&bytes, "items").unwrap();
424        assert_eq!(JsonbAccessor::array_len(items), Some(3));
425        let elem1 = JsonbAccessor::array_get(items, 1).unwrap();
426        assert_eq!(JsonbAccessor::as_i64(elem1), Some(2));
427    }
428
429    #[test]
430    fn test_large_object() {
431        let mut enc = JsonbEncoder::new();
432        let mut obj = serde_json::Map::new();
433        for i in 0..100 {
434            obj.insert(format!("field_{i:03}"), json!(i));
435        }
436        let bytes = enc.encode(&serde_json::Value::Object(obj));
437
438        // Binary search should find any field.
439        for i in 0..100 {
440            let key = format!("field_{i:03}");
441            let val = JsonbAccessor::get_field(&bytes, &key).unwrap();
442            assert_eq!(JsonbAccessor::as_i64(val), Some(i));
443        }
444        assert!(JsonbAccessor::get_field(&bytes, "nonexistent").is_none());
445    }
446
447    #[test]
448    fn test_unicode_keys() {
449        let mut enc = JsonbEncoder::new();
450        let bytes = enc.encode(&json!({"名前": "太郎", "年齢": 25}));
451
452        let name = JsonbAccessor::get_field(&bytes, "名前").unwrap();
453        assert_eq!(JsonbAccessor::as_str(name), Some("太郎"));
454
455        let age = JsonbAccessor::get_field(&bytes, "年齢").unwrap();
456        assert_eq!(JsonbAccessor::as_i64(age), Some(25));
457    }
458
459    #[test]
460    fn test_type_mismatch_returns_none() {
461        let mut enc = JsonbEncoder::new();
462        let bytes = enc.encode(&json!(42)); // INT64
463        assert!(JsonbAccessor::as_str(&bytes).is_none());
464        assert!(JsonbAccessor::as_bool(&bytes).is_none());
465        assert!(JsonbAccessor::as_f64(&bytes).is_none());
466    }
467
468    #[test]
469    fn test_empty_slice() {
470        // Empty slice is not null — null is tag 0x00.
471        assert!(!JsonbAccessor::is_null(&[]));
472        assert!(JsonbAccessor::as_bool(&[]).is_none());
473        assert!(JsonbAccessor::as_i64(&[]).is_none());
474        assert!(JsonbAccessor::as_f64(&[]).is_none());
475        assert!(JsonbAccessor::as_str(&[]).is_none());
476        assert!(JsonbAccessor::get_field(&[], "x").is_none());
477    }
478}