Skip to main content

velr_types/
cypher_order.rs

1use crate::codec::decode_property_value;
2use crate::list::ListValue;
3use crate::property::PropertyValueRef;
4use crate::storage::StorageValueRef;
5use crate::tag;
6use serde_json::Value;
7
8const RANK_EMPTY: u8 = 0x00;
9const RANK_MAP: u8 = 0x01;
10const RANK_NODE: u8 = 0x02;
11const RANK_RELATIONSHIP: u8 = 0x03;
12const RANK_LIST: u8 = 0x04;
13const RANK_PATH: u8 = 0x05;
14const RANK_OTHER: u8 = 0x06;
15const RANK_STRING: u8 = 0x07;
16const RANK_BOOLEAN: u8 = 0x08;
17const RANK_NUMBER: u8 = 0x09;
18const RANK_NAN: u8 = 0x0a;
19const RANK_NULL: u8 = 0x0b;
20
21const OTHER_TEMPORAL_DATE: u8 = 0x10;
22const OTHER_TEMPORAL_LOCAL_TIME: u8 = 0x11;
23const OTHER_TEMPORAL_ZONED_TIME: u8 = 0x12;
24const OTHER_TEMPORAL_LOCAL_DATETIME: u8 = 0x13;
25const OTHER_TEMPORAL_ZONED_DATETIME: u8 = 0x14;
26const OTHER_TEMPORAL_DURATION: u8 = 0x15;
27const OTHER_SPATIAL_POINT: u8 = 0x20;
28const OTHER_SPATIAL_GEOMETRY: u8 = 0x21;
29const OTHER_SPATIAL_GEOGRAPHY: u8 = 0x22;
30const OTHER_VECTOR: u8 = 0x40;
31const OTHER_BYTES: u8 = 0x41;
32
33const NUM_EXP_BIAS: i32 = 1074;
34const NUM_INF_EXPONENT: u16 = u16::MAX;
35
36#[derive(Debug)]
37pub enum OrderKeyError {
38    Decode(crate::error::DecodeError),
39    Encode(crate::error::EncodeError),
40    Json(serde_json::Error),
41    InvalidUtf8(std::str::Utf8Error),
42}
43
44impl std::fmt::Display for OrderKeyError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::Decode(err) => err.fmt(f),
48            Self::Encode(err) => err.fmt(f),
49            Self::Json(err) => err.fmt(f),
50            Self::InvalidUtf8(err) => err.fmt(f),
51        }
52    }
53}
54
55impl std::error::Error for OrderKeyError {}
56
57impl From<crate::error::DecodeError> for OrderKeyError {
58    fn from(value: crate::error::DecodeError) -> Self {
59        Self::Decode(value)
60    }
61}
62
63impl From<crate::error::EncodeError> for OrderKeyError {
64    fn from(value: crate::error::EncodeError) -> Self {
65        Self::Encode(value)
66    }
67}
68
69impl From<serde_json::Error> for OrderKeyError {
70    fn from(value: serde_json::Error) -> Self {
71        Self::Json(value)
72    }
73}
74
75impl From<std::str::Utf8Error> for OrderKeyError {
76    fn from(value: std::str::Utf8Error) -> Self {
77        Self::InvalidUtf8(value)
78    }
79}
80
81pub fn storage_value_to_cypher_order_key(
82    value: StorageValueRef<'_>,
83) -> Result<Vec<u8>, OrderKeyError> {
84    let mut out = Vec::with_capacity(initial_key_capacity(value));
85    append_storage_key(&mut out, value)?;
86    Ok(out)
87}
88
89fn initial_key_capacity(value: StorageValueRef<'_>) -> usize {
90    match value {
91        StorageValueRef::Null | StorageValueRef::Integer(_) | StorageValueRef::Real(_) => 12,
92        StorageValueRef::Text(v) => 2 + v.len().saturating_mul(2),
93        StorageValueRef::Blob(v) => v.len().saturating_mul(2).saturating_add(2).min(128).max(16),
94    }
95}
96
97fn append_storage_key(out: &mut Vec<u8>, value: StorageValueRef<'_>) -> Result<(), OrderKeyError> {
98    match value {
99        StorageValueRef::Null => out.push(RANK_NULL),
100        StorageValueRef::Integer(v) => append_i64_key(out, v),
101        StorageValueRef::Real(v) => append_f64_key(out, v),
102        StorageValueRef::Text(v) => append_string_key(out, v),
103        StorageValueRef::Blob(blob) => append_blob_key(out, blob)?,
104    }
105    Ok(())
106}
107
108fn append_blob_key(out: &mut Vec<u8>, blob: &[u8]) -> Result<(), OrderKeyError> {
109    let Some(first) = blob.first().copied() else {
110        out.push(RANK_OTHER);
111        append_bytes_terminated(out, blob);
112        return Ok(());
113    };
114
115    match first {
116        tag::RUNTIME_NODE_REF => append_runtime_ref_key(out, RANK_NODE, &blob[1..]),
117        tag::RUNTIME_REL_REF => append_runtime_ref_key(out, RANK_RELATIONSHIP, &blob[1..]),
118        tag::RUNTIME_PATH_REF => append_runtime_ref_key(out, RANK_PATH, &blob[1..]),
119        tag::RUNTIME_NAN => {
120            out.push(RANK_NAN);
121            Ok(())
122        }
123        tag::JSON | tag::MAP_JSON | tag::NODE_JSON | tag::RELATIONSHIP_JSON | tag::PATH_JSON => {
124            let json_text = std::str::from_utf8(&blob[1..])?;
125            let value: Value = serde_json::from_str(json_text)?;
126            match first {
127                tag::MAP_JSON => append_json_map_key(out, &value),
128                tag::NODE_JSON => append_json_entity_key(out, RANK_NODE, &value),
129                tag::RELATIONSHIP_JSON => append_json_entity_key(out, RANK_RELATIONSHIP, &value),
130                tag::PATH_JSON => append_json_path_key(out, &value),
131                _ => append_json_key(out, &value),
132            }
133        }
134        _ => match decode_property_value(StorageValueRef::Blob(blob)) {
135            Ok(value) => append_property_key(out, value.as_ref()),
136            Err(err) => {
137                if let Ok(text) = std::str::from_utf8(blob) {
138                    if let Ok(value) = serde_json::from_str::<Value>(text) {
139                        return append_json_key(out, &value);
140                    }
141                }
142                Err(err.into())
143            }
144        },
145    }
146}
147
148fn append_property_key(
149    out: &mut Vec<u8>,
150    value: PropertyValueRef<'_>,
151) -> Result<(), OrderKeyError> {
152    match value {
153        PropertyValueRef::Null => out.push(RANK_NULL),
154        PropertyValueRef::Bool(v) => append_bool_key(out, v),
155        PropertyValueRef::Integer(v) => append_i64_key(out, v),
156        PropertyValueRef::Float(v) => append_f64_key(out, v),
157        PropertyValueRef::String(v) => append_string_key(out, v),
158        PropertyValueRef::List(v) => append_list_key(out, v)?,
159
160        PropertyValueRef::Date(v) => {
161            append_other_subtype(out, OTHER_TEMPORAL_DATE);
162            append_i64_payload(out, v.epoch_day());
163        }
164        PropertyValueRef::LocalTime(v) => {
165            append_other_subtype(out, OTHER_TEMPORAL_LOCAL_TIME);
166            append_i64_payload(out, v.nanos_since_midnight());
167        }
168        PropertyValueRef::ZonedTime(v) => {
169            append_other_subtype(out, OTHER_TEMPORAL_ZONED_TIME);
170            append_i64_payload(out, v.utc_nanos_since_midnight());
171            append_i64_payload(out, v.time.nanos_since_midnight());
172            append_i64_payload(out, v.offset.seconds_east() as i64);
173        }
174        PropertyValueRef::LocalDateTime(v) => {
175            let (day, nanos) = v.local_epoch_day_and_nanos();
176            append_other_subtype(out, OTHER_TEMPORAL_LOCAL_DATETIME);
177            append_i64_payload(out, day);
178            append_i64_payload(out, nanos);
179        }
180        PropertyValueRef::ZonedDateTime(v) => {
181            let (day, nanos) = v.utc_epoch_day_and_nanos();
182            let (local_day, local_nanos) = v.datetime.local_epoch_day_and_nanos();
183            append_other_subtype(out, OTHER_TEMPORAL_ZONED_DATETIME);
184            append_i64_payload(out, day);
185            append_i64_payload(out, nanos);
186            append_i64_payload(out, local_day);
187            append_i64_payload(out, local_nanos);
188            append_i64_payload(out, v.offset.seconds_east() as i64);
189            append_string_payload(out, v.zone_id.as_deref().unwrap_or(""));
190        }
191        PropertyValueRef::Duration(v) => {
192            append_other_subtype(out, OTHER_TEMPORAL_DURATION);
193            append_string_payload(out, &v.to_string());
194        }
195
196        PropertyValueRef::Point(v) => {
197            append_other_subtype(out, OTHER_SPATIAL_POINT);
198            append_string_payload(out, &v.to_geojson_string()?);
199        }
200        PropertyValueRef::Geometry(v) => {
201            append_other_subtype(out, OTHER_SPATIAL_GEOMETRY);
202            append_string_payload(out, &v.to_geojson_string()?);
203        }
204        PropertyValueRef::Geography(v) => {
205            append_other_subtype(out, OTHER_SPATIAL_GEOGRAPHY);
206            append_string_payload(out, &v.to_geojson_string()?);
207        }
208        PropertyValueRef::Vector(v) => {
209            append_other_subtype(out, OTHER_VECTOR);
210            append_u64_be(out, v.len() as u64);
211            for elem in v.iter() {
212                match elem {
213                    crate::vector::VectorElem::I8(x) => append_i64_key(out, x as i64),
214                    crate::vector::VectorElem::I16(x) => append_i64_key(out, x as i64),
215                    crate::vector::VectorElem::I32(x) => append_i64_key(out, x as i64),
216                    crate::vector::VectorElem::I64(x) => append_i64_key(out, x),
217                    crate::vector::VectorElem::F32(x) => append_f64_key(out, x as f64),
218                    crate::vector::VectorElem::F64(x) => append_f64_key(out, x),
219                }
220            }
221        }
222        PropertyValueRef::Bytes(v) => {
223            append_other_subtype(out, OTHER_BYTES);
224            append_bytes_terminated(out, v);
225        }
226    }
227    Ok(())
228}
229
230fn append_runtime_ref_key(
231    out: &mut Vec<u8>,
232    rank: u8,
233    payload: &[u8],
234) -> Result<(), OrderKeyError> {
235    out.push(rank);
236    if let Ok(text) = std::str::from_utf8(payload) {
237        if let Some(id) = parse_runtime_ref_primary_i64(text) {
238            append_i64_payload(out, id);
239            return Ok(());
240        }
241    }
242    append_bytes_terminated(out, payload);
243    Ok(())
244}
245
246fn parse_runtime_ref_primary_i64(text: &str) -> Option<i64> {
247    let primary = text.split_once(':').map(|(head, _)| head).unwrap_or(text);
248    primary.parse::<i64>().ok()
249}
250
251fn append_json_key(out: &mut Vec<u8>, value: &Value) -> Result<(), OrderKeyError> {
252    match value {
253        Value::Null => out.push(RANK_NULL),
254        Value::Bool(v) => append_bool_key(out, *v),
255        Value::Number(v) => {
256            if let Some(i) = v.as_i64() {
257                append_i64_key(out, i);
258            } else if let Some(u) = v.as_u64() {
259                append_f64_key(out, u as f64);
260            } else if let Some(f) = v.as_f64() {
261                append_f64_key(out, f);
262            } else {
263                out.push(RANK_NAN);
264            }
265        }
266        Value::String(v) => append_string_key(out, v),
267        Value::Array(values) => append_json_list_key(out, values)?,
268        Value::Object(_) => append_json_map_key(out, value)?,
269    }
270    Ok(())
271}
272
273fn append_json_list_key(out: &mut Vec<u8>, values: &[Value]) -> Result<(), OrderKeyError> {
274    out.push(RANK_LIST);
275    for value in values {
276        append_json_key(out, value)?;
277    }
278    out.push(RANK_EMPTY);
279    Ok(())
280}
281
282fn append_json_map_key(out: &mut Vec<u8>, value: &Value) -> Result<(), OrderKeyError> {
283    let Value::Object(map) = value else {
284        append_json_key(out, value)?;
285        return Ok(());
286    };
287
288    out.push(RANK_MAP);
289    append_u64_be(out, map.len() as u64);
290
291    let mut entries = map.iter().collect::<Vec<_>>();
292    entries.sort_by(|a, b| a.0.cmp(b.0));
293
294    for (key, _) in &entries {
295        append_string_payload(out, key);
296    }
297    for (_, value) in entries {
298        append_json_key(out, value)?;
299    }
300
301    Ok(())
302}
303
304fn append_json_entity_key(out: &mut Vec<u8>, rank: u8, value: &Value) -> Result<(), OrderKeyError> {
305    let id = value
306        .get("identity")
307        .and_then(Value::as_i64)
308        .or_else(|| value.get("id").and_then(Value::as_i64))
309        .unwrap_or(0);
310    out.push(rank);
311    append_i64_payload(out, id);
312    Ok(())
313}
314
315fn append_json_path_key(out: &mut Vec<u8>, value: &Value) -> Result<(), OrderKeyError> {
316    let Value::Array(values) = value else {
317        append_json_key(out, value)?;
318        return Ok(());
319    };
320
321    out.push(RANK_PATH);
322    for value in values {
323        append_path_element_key(out, value)?;
324    }
325    out.push(RANK_EMPTY);
326    Ok(())
327}
328
329fn append_path_element_key(out: &mut Vec<u8>, value: &Value) -> Result<(), OrderKeyError> {
330    if value.get("labels").is_some() && value.get("identity").is_some() {
331        append_json_entity_key(out, RANK_NODE, value)
332    } else if value.get("type").is_some() && value.get("identity").is_some() {
333        append_json_entity_key(out, RANK_RELATIONSHIP, value)
334    } else {
335        append_json_key(out, value)
336    }
337}
338
339fn append_list_key(out: &mut Vec<u8>, list: &ListValue) -> Result<(), OrderKeyError> {
340    out.push(RANK_LIST);
341    for value in list.iter() {
342        append_property_key(out, value)?;
343    }
344    out.push(RANK_EMPTY);
345    Ok(())
346}
347
348fn append_bool_key(out: &mut Vec<u8>, value: bool) {
349    out.push(RANK_BOOLEAN);
350    out.push(u8::from(value));
351}
352
353fn append_string_key(out: &mut Vec<u8>, value: &str) {
354    out.push(RANK_STRING);
355    append_string_payload(out, value);
356}
357
358fn append_other_subtype(out: &mut Vec<u8>, subtype: u8) {
359    out.push(RANK_OTHER);
360    out.push(subtype);
361}
362
363fn append_string_payload(out: &mut Vec<u8>, value: &str) {
364    append_bytes_terminated(out, value.as_bytes());
365}
366
367fn append_bytes_terminated(out: &mut Vec<u8>, bytes: &[u8]) {
368    for byte in bytes {
369        out.push(0x01);
370        out.push(*byte);
371    }
372    out.push(RANK_EMPTY);
373}
374
375fn append_u64_be(out: &mut Vec<u8>, value: u64) {
376    out.extend_from_slice(&value.to_be_bytes());
377}
378
379fn append_i64_key(out: &mut Vec<u8>, value: i64) {
380    out.push(RANK_NUMBER);
381    append_i64_payload(out, value);
382}
383
384fn append_i64_payload(out: &mut Vec<u8>, value: i64) {
385    let (negative, magnitude) = if value < 0 {
386        (true, value.wrapping_neg() as u64)
387    } else {
388        (false, value as u64)
389    };
390
391    if magnitude == 0 {
392        append_zero_number_payload(out);
393        return;
394    }
395
396    let highest_bit = 63 - magnitude.leading_zeros();
397    let significand = magnitude << (63 - highest_bit);
398    append_finite_number_payload(out, negative, highest_bit as i32, significand);
399}
400
401fn append_f64_key(out: &mut Vec<u8>, value: f64) {
402    if value.is_nan() {
403        out.push(RANK_NAN);
404        return;
405    }
406
407    out.push(RANK_NUMBER);
408
409    if value == 0.0 {
410        append_zero_number_payload(out);
411        return;
412    }
413
414    if value.is_infinite() {
415        append_infinite_number_payload(out, value.is_sign_negative());
416        return;
417    }
418
419    let bits = value.to_bits();
420    let negative = (bits >> 63) != 0;
421    let raw_exp = ((bits >> 52) & 0x7ff) as u16;
422    let fraction = bits & ((1u64 << 52) - 1);
423
424    if raw_exp == 0 {
425        let highest_bit = 63 - fraction.leading_zeros();
426        let exponent = highest_bit as i32 - NUM_EXP_BIAS;
427        let significand = fraction << (63 - highest_bit);
428        append_finite_number_payload(out, negative, exponent, significand);
429    } else {
430        let exponent = raw_exp as i32 - 1023;
431        let significand = ((1u64 << 52) | fraction) << 11;
432        append_finite_number_payload(out, negative, exponent, significand);
433    }
434}
435
436fn append_zero_number_payload(out: &mut Vec<u8>) {
437    out.push(0x01);
438}
439
440fn append_infinite_number_payload(out: &mut Vec<u8>, negative: bool) {
441    append_number_magnitude_payload(out, negative, NUM_INF_EXPONENT, u64::MAX);
442}
443
444fn append_finite_number_payload(
445    out: &mut Vec<u8>,
446    negative: bool,
447    exponent: i32,
448    significand: u64,
449) {
450    let biased_exponent = (exponent + NUM_EXP_BIAS) as u16;
451    append_number_magnitude_payload(out, negative, biased_exponent, significand);
452}
453
454fn append_number_magnitude_payload(
455    out: &mut Vec<u8>,
456    negative: bool,
457    exponent: u16,
458    significand: u64,
459) {
460    out.push(if negative { 0x00 } else { 0x02 });
461
462    let mut exponent_bytes = exponent.to_be_bytes();
463    let mut significand_bytes = significand.to_be_bytes();
464    if negative {
465        for byte in &mut exponent_bytes {
466            *byte = !*byte;
467        }
468        for byte in &mut significand_bytes {
469            *byte = !*byte;
470        }
471    }
472
473    out.extend_from_slice(&exponent_bytes);
474    out.extend_from_slice(&significand_bytes);
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480    use crate::codec::encode_property_value;
481    use crate::list::ListValue;
482    use crate::property::PropertyValue;
483    use crate::storage::{StorageValue, StorageValueRef};
484    use std::hint::black_box;
485    use std::time::Instant;
486
487    fn key(value: StorageValueRef<'_>) -> Vec<u8> {
488        storage_value_to_cypher_order_key(value).unwrap()
489    }
490
491    fn storage_key(value: &StorageValue) -> Vec<u8> {
492        key(value.as_ref())
493    }
494
495    fn property_key(value: PropertyValue) -> Vec<u8> {
496        storage_key(&encode_property_value(&value).unwrap())
497    }
498
499    #[test]
500    fn numeric_order_keys_are_compact() {
501        let values = [
502            StorageValueRef::Real(f64::NEG_INFINITY),
503            StorageValueRef::Integer(i64::MIN),
504            StorageValueRef::Real(-1.5),
505            StorageValueRef::Integer(-1),
506            StorageValueRef::Integer(0),
507            StorageValueRef::Real(-0.0),
508            StorageValueRef::Real(0.5),
509            StorageValueRef::Integer(1),
510            StorageValueRef::Real(1.5),
511            StorageValueRef::Integer(i64::MAX),
512            StorageValueRef::Real(f64::INFINITY),
513        ];
514
515        for value in values {
516            assert!(
517                key(value).len() <= 12,
518                "numeric order keys should stay small enough for hot ORDER BY paths"
519            );
520        }
521    }
522
523    #[test]
524    fn numeric_sort_keys_preserve_mixed_integer_float_order() {
525        let ordered = [
526            StorageValueRef::Real(f64::NEG_INFINITY),
527            StorageValueRef::Integer(i64::MIN),
528            StorageValueRef::Real(-9_007_199_254_740_994.0),
529            StorageValueRef::Integer(-9_007_199_254_740_993),
530            StorageValueRef::Real(-9_007_199_254_740_992.0),
531            StorageValueRef::Integer(-2),
532            StorageValueRef::Real(-1.5),
533            StorageValueRef::Integer(-1),
534            StorageValueRef::Real(-0.5),
535            StorageValueRef::Integer(0),
536            StorageValueRef::Real(0.5),
537            StorageValueRef::Integer(1),
538            StorageValueRef::Real(1.5),
539            StorageValueRef::Integer(2),
540            StorageValueRef::Real(9_007_199_254_740_992.0),
541            StorageValueRef::Integer(9_007_199_254_740_993),
542            StorageValueRef::Real(9_007_199_254_740_994.0),
543            StorageValueRef::Integer(i64::MAX),
544            StorageValueRef::Real(f64::INFINITY),
545            StorageValueRef::Real(f64::NAN),
546            StorageValueRef::Null,
547        ];
548
549        for pair in ordered.windows(2) {
550            assert!(
551                key(pair[0]) < key(pair[1]),
552                "{:?} should sort before {:?}",
553                pair[0],
554                pair[1]
555            );
556        }
557
558        assert_eq!(
559            key(StorageValueRef::Integer(1)),
560            key(StorageValueRef::Real(1.0))
561        );
562        assert_eq!(
563            key(StorageValueRef::Integer(0)),
564            key(StorageValueRef::Real(-0.0))
565        );
566    }
567
568    #[test]
569    fn zoned_time_order_keys_use_utc_time_of_day() {
570        let mut values = vec![
571            "10:35-08:00",
572            "12:31:14.645876123+01:00",
573            "12:31:14.645876124+01:00",
574            "12:35:15+05:00",
575            "12:30:14.645876123+01:01",
576        ];
577
578        values.sort_by_key(|value| property_key(PropertyValue::ZonedTime(value.parse().unwrap())));
579
580        assert_eq!(
581            values,
582            vec![
583                "12:35:15+05:00",
584                "12:30:14.645876123+01:01",
585                "12:31:14.645876123+01:00",
586                "12:31:14.645876124+01:00",
587                "10:35-08:00",
588            ]
589        );
590    }
591
592    #[test]
593    fn zoned_datetime_order_keys_use_utc_instant() {
594        let mut values = vec![
595            "1984-10-11T12:30:14.000000012+00:15",
596            "1984-10-11T12:31:14.645876123+00:17",
597            "0001-01-01T01:01:01.000000001-11:59",
598            "9999-09-09T09:59:59.999999999+11:59",
599            "1980-12-11T12:31:14-11:59",
600        ];
601
602        values.sort_by_key(|value| {
603            property_key(PropertyValue::ZonedDateTime(value.parse().unwrap()))
604        });
605
606        assert_eq!(
607            values,
608            vec![
609                "0001-01-01T01:01:01.000000001-11:59",
610                "1980-12-11T12:31:14-11:59",
611                "1984-10-11T12:31:14.645876123+00:17",
612                "1984-10-11T12:30:14.000000012+00:15",
613                "9999-09-09T09:59:59.999999999+11:59",
614            ]
615        );
616    }
617
618    #[test]
619    #[ignore = "microbenchmark; run explicitly with --ignored --nocapture"]
620    fn cypher_order_key_micro_benchmark() {
621        let list = encode_property_value(&PropertyValue::List(ListValue::Generic(vec![
622            PropertyValue::Integer(42),
623            PropertyValue::String("alpha".to_string()),
624            PropertyValue::Bool(true),
625            PropertyValue::Null,
626        ])))
627        .unwrap();
628        let bool_value = encode_property_value(&PropertyValue::Bool(true)).unwrap();
629        let raw_json_list = StorageValue::Blob(br#"[1,"alpha",false,null]"#.to_vec());
630
631        let samples = [
632            StorageValue::Null,
633            StorageValue::Integer(-9_007_199_254_740_993),
634            StorageValue::Integer(9_007_199_254_740_993),
635            StorageValue::Real(-12345.25),
636            StorageValue::Real(12345.25),
637            StorageValue::Text("ordering-hot-path".to_string()),
638            bool_value,
639            list,
640            raw_json_list,
641        ];
642
643        let iterations: usize = std::env::var("VELR_ORDER_KEY_BENCH_ITERS")
644            .ok()
645            .and_then(|value| value.parse().ok())
646            .unwrap_or(if cfg!(debug_assertions) {
647                20_000
648            } else {
649                500_000
650            });
651
652        let start = Instant::now();
653        let mut bytes = 0usize;
654        for i in 0..iterations {
655            let sample = black_box(&samples[i % samples.len()]);
656            let sort_key = storage_key(sample);
657            bytes = bytes.wrapping_add(sort_key.len());
658            black_box(&sort_key);
659        }
660        let elapsed = start.elapsed();
661        let ns_per_key = elapsed.as_nanos() as f64 / iterations as f64;
662
663        eprintln!(
664            "cypher_order_key_micro_benchmark: iterations={iterations} elapsed={elapsed:?} ns_per_key={ns_per_key:.1} bytes={bytes}"
665        );
666
667        if let Ok(max_ns_per_key) = std::env::var("VELR_ORDER_KEY_MAX_NS") {
668            let max_ns_per_key: f64 = max_ns_per_key.parse().unwrap();
669            assert!(
670                ns_per_key <= max_ns_per_key,
671                "sort key encoding took {ns_per_key:.1} ns/key, expected <= {max_ns_per_key:.1} ns/key"
672            );
673        }
674    }
675}