Skip to main content

laminar_connectors/schema/json/jsonb/
mod.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;