Skip to main content

hidane_core/
key.rs

1//! Order-preserving byte encoding of values and document names.
2//!
3//! For any two values `a` and `b`:
4//!
5//! ```text
6//! order::compare(a, b) == encode_value(a).cmp(&encode_value(b))
7//! ```
8//!
9//! Values that compare equal (`1` and `1.0`, `-0.0` and `0`, two NaNs, `{a: 1}` and
10//! `{a: 1.0}`) encode to identical bytes, so an equality filter is a single key lookup and a
11//! range filter is a single key range. Every encoding is self-delimiting, so encoded values can
12//! be concatenated into composite index keys (field value, then document name).
13//!
14//! Layout: a type tag, then a type-specific payload.
15//!
16//! | tag  | type       | payload |
17//! |------|------------|---------|
18//! | 0x05 | null       | — |
19//! | 0x0A | boolean    | 0x00 false / 0x01 true |
20//! | 0x0F | NaN        | — |
21//! | 0x14 | number     | nearest double (8 bytes, sortable), then the integer's distance to it (2 bytes, sortable) |
22//! | 0x19 | timestamp  | seconds (8 bytes, sortable), nanos (4 bytes) |
23//! | 0x1E | string     | escaped UTF-8, terminator |
24//! | 0x23 | bytes      | escaped bytes, terminator |
25//! | 0x28 | reference  | path encoding of the resource name |
26//! | 0x2D | geo point  | latitude, longitude (8 bytes each, sortable) |
27//! | 0x32 | array      | element encodings, then 0x00 |
28//! | 0x37 | vector     | length (4 bytes), element encodings |
29//! | 0x3C | map        | (escaped key, terminator, value encoding)*, then 0x00 0x00 |
30//! | 0x41 | expression | — |
31//!
32//! Escaping: inside strings, bytes, keys and path segments 0x00 becomes 0x00 0xFF and the
33//! terminator is 0x00 0x01, so a prefix sorts before every extension of it.
34//!
35//! The BSON types of an enterprise database ([`crate::bson`]) are encoded as the maps they are
36//! written as, not in their own order: nothing stores or looks them up by key.
37//!
38//! Numbers: an integer and a double compare by exact value. The integer is rounded to the
39//! nearest double; rounding is monotonic, so different rounded values already order the
40//! numbers. When the rounded values are equal, the integer's exact distance from that double
41//! (at most 1024 in magnitude, 0 for doubles) breaks the tie.
42
43use hidane_proto::google::firestore::v1::{Value, value::ValueType};
44
45use crate::order::{is_vector, numeric_id, vector_elements};
46
47const NULL: u8 = 0x05;
48const BOOLEAN: u8 = 0x0A;
49const NAN: u8 = 0x0F;
50const NUMBER: u8 = 0x14;
51const TIMESTAMP: u8 = 0x19;
52const STRING: u8 = 0x1E;
53const BYTES: u8 = 0x23;
54const REFERENCE: u8 = 0x28;
55const GEO_POINT: u8 = 0x2D;
56const ARRAY: u8 = 0x32;
57const VECTOR: u8 = 0x37;
58const MAP: u8 = 0x3C;
59const EXPRESSION: u8 = 0x41;
60
61const SEQUENCE_END: u8 = 0x00;
62const ESCAPE: [u8; 2] = [0x00, 0xFF];
63const TERMINATOR: [u8; 2] = [0x00, 0x01];
64const MAP_END: [u8; 2] = [0x00, 0x00];
65
66const PATH_END: u8 = 0x00;
67const PATH_NUMERIC_SEGMENT: u8 = 0x01;
68const PATH_STRING_SEGMENT: u8 = 0x02;
69const PATH_AFTER_DESCENDANTS: u8 = 0x03;
70
71/// Encodes `value` so that byte order equals Firestore value order.
72pub fn encode_value(value: &Value) -> Vec<u8> {
73    let mut out = Vec::new();
74    write_value(value, &mut out);
75    out
76}
77
78/// Escaped bytes plus terminator, as used for strings and map keys. Prefix of a
79/// collection-group index key.
80pub fn encode_escaped(bytes: &[u8]) -> Vec<u8> {
81    let mut out = Vec::new();
82    write_escaped(bytes, &mut out);
83    out
84}
85
86pub fn write_value(value: &Value, out: &mut Vec<u8>) {
87    match &value.value_type {
88        None | Some(ValueType::NullValue(_)) => out.push(NULL),
89        Some(ValueType::BooleanValue(b)) => out.extend([BOOLEAN, u8::from(*b)]),
90        Some(ValueType::IntegerValue(i)) => write_integer(*i, out),
91        Some(ValueType::DoubleValue(d)) => write_double(*d, out),
92        Some(ValueType::TimestampValue(ts)) => {
93            out.push(TIMESTAMP);
94            out.extend(sortable_i64(ts.seconds));
95            out.extend(ts.nanos.to_be_bytes());
96        }
97        Some(ValueType::StringValue(s)) => {
98            out.push(STRING);
99            write_escaped(s.as_bytes(), out);
100        }
101        Some(ValueType::BytesValue(b)) => {
102            out.push(BYTES);
103            write_escaped(b, out);
104        }
105        Some(ValueType::ReferenceValue(name)) => {
106            out.push(REFERENCE);
107            write_path(name.split('/'), out);
108        }
109        Some(ValueType::GeoPointValue(point)) => {
110            out.push(GEO_POINT);
111            out.extend(sortable_f64(point.latitude));
112            out.extend(sortable_f64(point.longitude));
113        }
114        Some(ValueType::ArrayValue(array)) => {
115            out.push(ARRAY);
116            for element in &array.values {
117                write_value(element, out);
118            }
119            out.push(SEQUENCE_END);
120        }
121        Some(ValueType::MapValue(map)) if is_vector(map) => {
122            let elements = vector_elements(map);
123            out.push(VECTOR);
124            out.extend(
125                u32::try_from(elements.len())
126                    .unwrap_or(u32::MAX)
127                    .to_be_bytes(),
128            );
129            for element in elements {
130                write_value(element, out);
131            }
132        }
133        Some(ValueType::MapValue(map)) => {
134            out.push(MAP);
135            // UTF-8 key order, as Firestore compares maps.
136            for (key, field) in map.fields.sorted() {
137                write_escaped(key.as_bytes(), out);
138                write_value(field, out);
139            }
140            out.extend(MAP_END);
141        }
142        Some(
143            ValueType::FieldReferenceValue(_)
144            | ValueType::VariableReferenceValue(_)
145            | ValueType::FunctionValue(_)
146            | ValueType::PipelineValue(_),
147        ) => out.push(EXPRESSION),
148    }
149}
150
151/// Encodes a document name (or any slash-separated resource path) so that byte order equals
152/// `__name__` order. Collection-group scans use the same encoding: documents in a group sort
153/// by their full path.
154pub fn encode_path<'a>(segments: impl IntoIterator<Item = &'a str>) -> Vec<u8> {
155    let mut out = Vec::new();
156    write_path(segments, &mut out);
157    out
158}
159
160pub fn write_path<'a>(segments: impl IntoIterator<Item = &'a str>, out: &mut Vec<u8>) {
161    write_path_prefix(segments, out);
162    out.push(PATH_END);
163}
164
165/// The encoding of `segments` without the end marker: every path that starts with these
166/// segments (the path itself and all its descendants) has an encoding that starts with it.
167pub fn encode_path_prefix<'a>(segments: impl IntoIterator<Item = &'a str>) -> Vec<u8> {
168    let mut out = Vec::new();
169    write_path_prefix(segments, &mut out);
170    out
171}
172
173/// An exclusive upper bound for the encodings of `prefix` and all its descendants: their next
174/// byte after the prefix is the end marker or a segment marker, all below this one.
175pub fn path_subtree_end(prefix: &[u8]) -> Vec<u8> {
176    let mut end = prefix.to_vec();
177    end.push(PATH_AFTER_DESCENDANTS);
178    end
179}
180
181fn write_path_prefix<'a>(segments: impl IntoIterator<Item = &'a str>, out: &mut Vec<u8>) {
182    for segment in segments {
183        match numeric_id(segment) {
184            Some(id) => {
185                out.push(PATH_NUMERIC_SEGMENT);
186                out.extend(sortable_i64(id));
187            }
188            None => {
189                out.push(PATH_STRING_SEGMENT);
190                write_escaped(segment.as_bytes(), out);
191            }
192        }
193    }
194}
195
196fn write_integer(i: i64, out: &mut Vec<u8>) {
197    #[allow(clippy::cast_precision_loss)]
198    let nearest = i as f64;
199    // `nearest` is an integral double within [-2^63, 2^63], so the conversion is exact.
200    #[allow(clippy::cast_possible_truncation)]
201    let distance = i128::from(i) - nearest as i128;
202    out.push(NUMBER);
203    out.extend(sortable_f64(nearest));
204    out.extend(sortable_i16(
205        i16::try_from(distance).expect("|i - round(i)| <= 1024"),
206    ));
207}
208
209fn write_double(d: f64, out: &mut Vec<u8>) {
210    if d.is_nan() {
211        out.push(NAN);
212        return;
213    }
214    out.push(NUMBER);
215    out.extend(sortable_f64(d));
216    out.extend(sortable_i16(0));
217}
218
219fn write_escaped(bytes: &[u8], out: &mut Vec<u8>) {
220    for &b in bytes {
221        if b == 0x00 {
222            out.extend(ESCAPE);
223        } else {
224            out.push(b);
225        }
226    }
227    out.extend(TERMINATOR);
228}
229
230/// Big-endian with the sign bit flipped: byte order equals numeric order.
231fn sortable_i64(v: i64) -> [u8; 8] {
232    (v.cast_unsigned() ^ (1 << 63)).to_be_bytes()
233}
234
235fn sortable_i16(v: i16) -> [u8; 2] {
236    (v.cast_unsigned() ^ (1 << 15)).to_be_bytes()
237}
238
239/// IEEE-754 bits made byte-comparable: flip every bit of negatives, only the sign bit of
240/// positives. `-0.0` is folded into `0.0` first because Firestore treats them as equal.
241fn sortable_f64(v: f64) -> [u8; 8] {
242    let v = if v == 0.0 { 0.0 } else { v };
243    let bits = v.to_bits();
244    let bits = if bits >> 63 == 1 {
245        !bits
246    } else {
247        bits ^ (1 << 63)
248    };
249    bits.to_be_bytes()
250}