Skip to main content

nodedb_query/msgpack_scan/
compare.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Byte-level comparison and hashing for MessagePack field values.
4//!
5//! Operates on raw byte ranges returned by `extract_field`. Used for
6//! join key matching, GROUP BY key deduplication, ORDER BY, and DISTINCT.
7
8use std::cmp::Ordering;
9use std::hash::{BuildHasher, Hasher};
10
11use nodedb_types::read_instant;
12
13use crate::msgpack_scan::reader::{read_integer, read_null, read_numeric, str_bounds};
14use crate::numeric_cmp::cmp_numeric;
15
16/// Hash the raw bytes of a MessagePack value at `range` within `buf`.
17/// Uses a fast non-cryptographic hash suitable for hash joins and GROUP BY.
18///
19/// For canonical-encoded documents (integers in smallest form, sorted keys),
20/// semantically equal values produce identical byte sequences and thus
21/// identical hashes.
22pub fn hash_field_bytes(buf: &[u8], range: (usize, usize)) -> u64 {
23    let slice = match buf.get(range.0..range.1) {
24        Some(s) => s,
25        None => return 0,
26    };
27    let hasher_builder = std::collections::hash_map::RandomState::new();
28    let mut hasher = hasher_builder.build_hasher();
29    hasher.write(slice);
30    hasher.finish()
31}
32
33/// Hash the raw bytes using a provided `RandomState` for consistent hashing
34/// within a single query (all docs hashed with the same seed).
35pub fn hash_field_bytes_with(
36    buf: &[u8],
37    range: (usize, usize),
38    state: &std::collections::hash_map::RandomState,
39) -> u64 {
40    let slice = match buf.get(range.0..range.1) {
41        Some(s) => s,
42        None => return 0,
43    };
44    let mut hasher = state.build_hasher();
45    hasher.write(slice);
46    hasher.finish()
47}
48
49/// Compare two MessagePack values by their decoded content.
50///
51/// Comparison order:
52/// 1. Null < Bool < Number < Instant < String < Binary < Array < Map < Ext
53/// 2. Within numbers: exact numeric order; integers (`uint64` included)
54///    never round through f64, an integer against a float compares
55///    exactly, and NaN sorts above every number and equals NaN
56/// 3. Within instants: by kind (UTC before naive), then signed epoch micros
57/// 4. Within strings: lexicographic on raw bytes (valid UTF-8 guarantees
58///    byte order = Unicode code-point order for ASCII/Latin-1)
59/// 5. Fallback: raw byte comparison
60pub fn compare_field_bytes(
61    a_buf: &[u8],
62    a_range: (usize, usize),
63    b_buf: &[u8],
64    b_range: (usize, usize),
65) -> Ordering {
66    let a_off = a_range.0;
67    let b_off = b_range.0;
68
69    let a_tag = match a_buf.get(a_off) {
70        Some(&t) => t,
71        None => return Ordering::Less,
72    };
73    let b_tag = match b_buf.get(b_off) {
74        Some(&t) => t,
75        None => return Ordering::Greater,
76    };
77
78    let a_type = type_rank(a_buf, a_off, a_tag);
79    let b_type = type_rank(b_buf, b_off, b_tag);
80
81    if a_type != b_type {
82        return a_type.cmp(&b_type);
83    }
84
85    match a_type {
86        RANK_NULL => Ordering::Equal,
87        RANK_BOOL => {
88            let a_val = a_tag == 0xc3; // true
89            let b_val = b_tag == 0xc3;
90            a_val.cmp(&b_val)
91        }
92        RANK_NUMBER => {
93            // Exact: integers never round through f64. NaN sorts above
94            // every number and equals NaN, so the order is total.
95            match (read_numeric(a_buf, a_off), read_numeric(b_buf, b_off)) {
96                (Some(a), Some(b)) => cmp_numeric(a, b),
97                (Some(_), None) => Ordering::Greater,
98                (None, Some(_)) => Ordering::Less,
99                (None, None) => Ordering::Equal,
100            }
101        }
102        RANK_INSTANT => {
103            // Both sides decoded as instants by `type_rank`. Kind first, then
104            // signed micros: the raw payload is not byte-comparable below 0.
105            match (read_instant(a_buf, a_off), read_instant(b_buf, b_off)) {
106                (Some((a_kind, a_us)), Some((b_kind, b_us))) => a_kind
107                    .ext_type()
108                    .cmp(&b_kind.ext_type())
109                    .then(a_us.cmp(&b_us)),
110                (Some(_), None) => Ordering::Greater,
111                (None, Some(_)) => Ordering::Less,
112                (None, None) => Ordering::Equal,
113            }
114        }
115        RANK_STRING => {
116            // string — compare raw bytes
117            match (str_bounds(a_buf, a_off), str_bounds(b_buf, b_off)) {
118                (Some((a_s, a_l)), Some((b_s, b_l))) => {
119                    let a_bytes = &a_buf[a_s..a_s + a_l];
120                    let b_bytes = &b_buf[b_s..b_s + b_l];
121                    a_bytes.cmp(b_bytes)
122                }
123                _ => Ordering::Equal,
124            }
125        }
126        _ => {
127            // binary, array, map, ext — fallback to raw byte comparison
128            let a_slice = &a_buf[a_range.0..a_range.1];
129            let b_slice = &b_buf[b_range.0..b_range.1];
130            a_slice.cmp(b_slice)
131        }
132    }
133}
134
135/// Compare two integer MessagePack values exactly, `uint64` included.
136/// Useful when the caller knows both values are integers.
137pub fn compare_field_i64(a_buf: &[u8], a_off: usize, b_buf: &[u8], b_off: usize) -> Ordering {
138    match (read_integer(a_buf, a_off), read_integer(b_buf, b_off)) {
139        (Some(a), Some(b)) => a.cmp(&b),
140        (Some(_), None) => Ordering::Greater,
141        (None, Some(_)) => Ordering::Less,
142        (None, None) => Ordering::Equal,
143    }
144}
145
146/// Check if two MessagePack values are byte-identical.
147/// For canonical-encoded documents, byte equality implies semantic equality.
148pub fn field_bytes_eq(
149    a_buf: &[u8],
150    a_range: (usize, usize),
151    b_buf: &[u8],
152    b_range: (usize, usize),
153) -> bool {
154    let a_slice = match a_buf.get(a_range.0..a_range.1) {
155        Some(s) => s,
156        None => return false,
157    };
158    let b_slice = match b_buf.get(b_range.0..b_range.1) {
159        Some(s) => s,
160        None => return false,
161    };
162    a_slice == b_slice
163}
164
165/// Check if a field value is null without extracting it.
166pub fn is_field_null(buf: &[u8], range: (usize, usize)) -> bool {
167    read_null(buf, range.0)
168}
169
170const RANK_NULL: u8 = 0;
171const RANK_BOOL: u8 = 1;
172const RANK_NUMBER: u8 = 2;
173const RANK_INSTANT: u8 = 3;
174const RANK_STRING: u8 = 4;
175const RANK_BINARY: u8 = 5;
176const RANK_ARRAY: u8 = 6;
177const RANK_MAP: u8 = 7;
178const RANK_EXT: u8 = 8;
179const RANK_UNKNOWN: u8 = 9;
180
181/// Type rank for cross-type ordering. Lower rank = sorts first.
182/// Null < Bool < Number < Instant < String < Binary < Array < Map < Ext
183///
184/// An instant is a complete `fixext8` of type 1 / 2 at `off`. A `fixext8`
185/// of any other type, or a truncated one, ranks as ext.
186fn type_rank(buf: &[u8], off: usize, tag: u8) -> u8 {
187    match tag {
188        0xc0 => RANK_NULL,
189        0xc2 | 0xc3 => RANK_BOOL,
190        0x00..=0x7f | 0xe0..=0xff => RANK_NUMBER, // fixint
191        0xca..=0xd3 => RANK_NUMBER,               // float/uint/int
192        0xa0..=0xbf | 0xd9..=0xdb => RANK_STRING,
193        0xc4..=0xc6 => RANK_BINARY,
194        0x90..=0x9f | 0xdc | 0xdd => RANK_ARRAY,
195        0x80..=0x8f | 0xde | 0xdf => RANK_MAP,
196        0xd7 => {
197            if read_instant(buf, off).is_some() {
198                RANK_INSTANT
199            } else {
200                RANK_EXT
201            }
202        }
203        0xc7..=0xc9 | 0xd4..=0xd6 | 0xd8 => RANK_EXT,
204        _ => RANK_UNKNOWN,
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use serde_json::json;
212
213    fn encode(v: &serde_json::Value) -> Vec<u8> {
214        nodedb_types::json_msgpack::json_to_msgpack(v).expect("encode")
215    }
216
217    fn val_range(buf: &[u8]) -> (usize, usize) {
218        (0, buf.len())
219    }
220
221    #[test]
222    fn hash_same_bytes_same_hash() {
223        let buf = encode(&json!(42));
224        let state = std::collections::hash_map::RandomState::new();
225        let h1 = hash_field_bytes_with(&buf, val_range(&buf), &state);
226        let h2 = hash_field_bytes_with(&buf, val_range(&buf), &state);
227        assert_eq!(h1, h2);
228    }
229
230    #[test]
231    fn hash_different_values_likely_differ() {
232        let buf1 = encode(&json!(42));
233        let buf2 = encode(&json!(43));
234        let state = std::collections::hash_map::RandomState::new();
235        let h1 = hash_field_bytes_with(&buf1, val_range(&buf1), &state);
236        let h2 = hash_field_bytes_with(&buf2, val_range(&buf2), &state);
237        assert_ne!(h1, h2);
238    }
239
240    #[test]
241    fn compare_integers() {
242        let a = encode(&json!(10));
243        let b = encode(&json!(20));
244        assert_eq!(
245            compare_field_bytes(&a, val_range(&a), &b, val_range(&b)),
246            Ordering::Less
247        );
248        assert_eq!(
249            compare_field_bytes(&b, val_range(&b), &a, val_range(&a)),
250            Ordering::Greater
251        );
252        assert_eq!(
253            compare_field_bytes(&a, val_range(&a), &a, val_range(&a)),
254            Ordering::Equal
255        );
256    }
257
258    #[test]
259    fn compare_strings() {
260        let a = encode(&json!("apple"));
261        let b = encode(&json!("banana"));
262        assert_eq!(
263            compare_field_bytes(&a, val_range(&a), &b, val_range(&b)),
264            Ordering::Less
265        );
266    }
267
268    #[test]
269    fn compare_cross_type_null_vs_number() {
270        let a = encode(&json!(null));
271        let b = encode(&json!(42));
272        assert_eq!(
273            compare_field_bytes(&a, val_range(&a), &b, val_range(&b)),
274            Ordering::Less
275        );
276    }
277
278    #[test]
279    fn compare_cross_type_string_vs_number() {
280        let a = encode(&json!(42));
281        let b = encode(&json!("hello"));
282        assert_eq!(
283            compare_field_bytes(&a, val_range(&a), &b, val_range(&b)),
284            Ordering::Less
285        );
286    }
287
288    #[test]
289    fn compare_booleans() {
290        let a = encode(&json!(false));
291        let b = encode(&json!(true));
292        assert_eq!(
293            compare_field_bytes(&a, val_range(&a), &b, val_range(&b)),
294            Ordering::Less
295        );
296    }
297
298    #[test]
299    fn compare_negative_integers() {
300        let a = encode(&json!(-10));
301        let b = encode(&json!(-5));
302        assert_eq!(
303            compare_field_bytes(&a, val_range(&a), &b, val_range(&b)),
304            Ordering::Less
305        );
306    }
307
308    #[test]
309    fn field_bytes_eq_works() {
310        let a = encode(&json!("test"));
311        let b = encode(&json!("test"));
312        let c = encode(&json!("other"));
313        assert!(field_bytes_eq(&a, val_range(&a), &b, val_range(&b)));
314        assert!(!field_bytes_eq(&a, val_range(&a), &c, val_range(&c)));
315    }
316
317    #[test]
318    fn is_field_null_works() {
319        let null_buf = encode(&json!(null));
320        let int_buf = encode(&json!(42));
321        assert!(is_field_null(&null_buf, val_range(&null_buf)));
322        assert!(!is_field_null(&int_buf, val_range(&int_buf)));
323    }
324
325    #[test]
326    fn compare_floats() {
327        let a = encode(&json!(1.5));
328        let b = encode(&json!(2.5));
329        assert_eq!(
330            compare_field_bytes(&a, val_range(&a), &b, val_range(&b)),
331            Ordering::Less
332        );
333    }
334
335    fn cmp(a: &serde_json::Value, b: &serde_json::Value) -> Ordering {
336        let (a, b) = (encode(a), encode(b));
337        compare_field_bytes(&a, val_range(&a), &b, val_range(&b))
338    }
339
340    #[test]
341    fn integers_past_two_pow_53_compare_exactly() {
342        let above = json!(9_007_199_254_740_993_i64);
343        let at = json!(9_007_199_254_740_992_i64);
344        assert_eq!(cmp(&above, &at), Ordering::Greater);
345        assert_eq!(cmp(&at, &above), Ordering::Less);
346        assert_eq!(cmp(&above, &above), Ordering::Equal);
347    }
348
349    #[test]
350    fn nanosecond_timestamps_compare_exactly() {
351        let t1 = json!(1_700_000_000_000_000_001_i64);
352        let t2 = json!(1_700_000_000_000_000_002_i64);
353        assert_eq!(cmp(&t1, &t2), Ordering::Less);
354        assert_eq!(cmp(&t2, &t1), Ordering::Greater);
355    }
356
357    #[test]
358    fn uint64_above_i64_max_orders_above_every_i64() {
359        assert_eq!(cmp(&json!(u64::MAX), &json!(i64::MAX)), Ordering::Greater);
360        assert_eq!(cmp(&json!(u64::MAX - 1), &json!(u64::MAX)), Ordering::Less);
361        assert_eq!(cmp(&json!(i64::MIN), &json!(u64::MAX)), Ordering::Less);
362        let (a, b) = (encode(&json!(u64::MAX)), encode(&json!(-1)));
363        assert_eq!(compare_field_i64(&a, 0, &b, 0), Ordering::Greater);
364    }
365
366    #[test]
367    fn integer_against_float_compares_exactly() {
368        let above = json!(9_007_199_254_740_993_i64);
369        let float = json!(9_007_199_254_740_992.0_f64);
370        assert_eq!(cmp(&above, &float), Ordering::Greater);
371        assert_eq!(cmp(&float, &above), Ordering::Less);
372        assert_eq!(cmp(&json!(2), &json!(2.5)), Ordering::Less);
373        assert_eq!(cmp(&json!(3), &json!(3.0)), Ordering::Equal);
374    }
375
376    /// NaN sorts above every number and equals NaN, so a sort over raw
377    /// cells is total.
378    #[test]
379    fn nan_cells_sort_above_every_number() {
380        let float = |f: f64| {
381            nodedb_types::value_to_msgpack(&nodedb_types::Value::Float(f)).expect("encode float")
382        };
383        let nan = float(f64::NAN);
384        let inf = float(f64::INFINITY);
385        let max = encode(&json!(u64::MAX));
386        assert_eq!(
387            compare_field_bytes(&nan, val_range(&nan), &inf, val_range(&inf)),
388            Ordering::Greater
389        );
390        assert_eq!(
391            compare_field_bytes(&max, val_range(&max), &nan, val_range(&nan)),
392            Ordering::Less
393        );
394        assert_eq!(
395            compare_field_bytes(&nan, val_range(&nan), &nan, val_range(&nan)),
396            Ordering::Equal
397        );
398        let mut cells = [nan.clone(), encode(&json!(2)), float(-1.5), nan, max];
399        cells.sort_by(|a, b| compare_field_bytes(a, val_range(a), b, val_range(b)));
400        assert_eq!(cells[0], float(-1.5));
401        assert_eq!(cells[1], encode(&json!(2)));
402        assert_eq!(cells[2], encode(&json!(u64::MAX)));
403    }
404
405    #[test]
406    fn numbers_keep_cross_rank_order() {
407        assert_eq!(cmp(&json!(true), &json!(u64::MAX)), Ordering::Less);
408        assert_eq!(cmp(&json!(u64::MAX), &json!("")), Ordering::Less);
409        assert_eq!(cmp(&json!(null), &json!(-1.5)), Ordering::Less);
410    }
411
412    #[test]
413    fn compare_instants_negative_micros() {
414        use nodedb_types::{InstantKind, write_instant};
415        let mut a = Vec::new();
416        write_instant(&mut a, InstantKind::Utc, -10);
417        let mut b = Vec::new();
418        write_instant(&mut b, InstantKind::Utc, -5);
419        let mut c = Vec::new();
420        write_instant(&mut c, InstantKind::Utc, 1);
421        assert_eq!(
422            compare_field_bytes(&a, val_range(&a), &b, val_range(&b)),
423            Ordering::Less
424        );
425        assert_eq!(
426            compare_field_bytes(&a, val_range(&a), &c, val_range(&c)),
427            Ordering::Less
428        );
429        assert_eq!(
430            compare_field_bytes(&c, val_range(&c), &b, val_range(&b)),
431            Ordering::Greater
432        );
433        assert_eq!(
434            compare_field_bytes(&a, val_range(&a), &a, val_range(&a)),
435            Ordering::Equal
436        );
437    }
438
439    #[test]
440    fn compare_instant_kinds_order_utc_first() {
441        use nodedb_types::{InstantKind, write_instant};
442        let mut utc = Vec::new();
443        write_instant(&mut utc, InstantKind::Utc, 100);
444        let mut naive = Vec::new();
445        write_instant(&mut naive, InstantKind::Naive, 1);
446        assert_eq!(
447            compare_field_bytes(&utc, val_range(&utc), &naive, val_range(&naive)),
448            Ordering::Less
449        );
450    }
451
452    #[test]
453    fn compare_instant_vs_integer_uses_rank() {
454        use nodedb_types::{InstantKind, write_instant};
455        // A negative instant has a payload starting 0xff, above any fixint
456        // byte. Rank places it after every number and before every string.
457        let mut inst = Vec::new();
458        write_instant(&mut inst, InstantKind::Naive, -1);
459        let int = encode(&json!(i64::MAX));
460        let s = encode(&json!(""));
461        assert_eq!(
462            compare_field_bytes(&int, val_range(&int), &inst, val_range(&inst)),
463            Ordering::Less
464        );
465        assert_eq!(
466            compare_field_bytes(&inst, val_range(&inst), &s, val_range(&s)),
467            Ordering::Less
468        );
469    }
470
471    #[test]
472    fn unknown_fixext8_ranks_as_ext() {
473        use nodedb_types::{InstantKind, write_instant};
474        let mut inst = Vec::new();
475        write_instant(&mut inst, InstantKind::Utc, 0);
476        let other = [0xd7, 0x09, 0, 0, 0, 0, 0, 0, 0, 0];
477        assert_eq!(
478            compare_field_bytes(&inst, val_range(&inst), &other, val_range(&other)),
479            Ordering::Less
480        );
481    }
482
483    #[test]
484    fn hash_from_extracted_field() {
485        let buf = encode(&json!({"id": 42}));
486        let range = crate::msgpack_scan::field::extract_field(&buf, 0, "id").unwrap();
487        let h = hash_field_bytes(&buf, range);
488        assert_ne!(h, 0);
489    }
490}