1use std::{
2 any::{Any, TypeId as StdTypeId},
3 collections::HashMap,
4 path::Path,
5 sync::Arc,
6};
7
8use parking_lot::ReentrantMutex;
9use serde::Serialize;
10use tracing::instrument;
11use zerocopy::{FromBytes, IntoBytes};
12
13use ahash::{AHashMap, AHashSet};
14
15use crate::matrices::MatrixSet;
16use crate::{
17 csr::{CsrCache, CsrSnapshot},
18 error::Error,
19 schema::{
20 AdjEntry, DirectedNeighborEntry, EdgeId, EdgeRecord, LabelId, Language, NeighborEntry,
21 NodeId, NodeRecord, PropKeyId, PropValue, TypeId, WeightedPath,
22 },
23 storage::{
24 fts,
25 ids::{
26 adjust_label_count, adjust_type_count, alloc_edge_id, alloc_node_id, get_label,
27 get_or_create_label, get_or_create_prop_key, get_or_create_type, get_prop_key,
28 get_prop_key_name, get_type,
29 },
30 lmdb::Storage,
31 props,
32 },
33};
34
35pub mod algo;
36pub mod edge;
37pub mod fts_mod;
38pub mod graphblas;
39pub mod index;
40pub mod node;
41pub mod stats;
42pub mod txn;
43pub mod vector;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
47pub enum DegreeDirection {
48 In,
50 Out,
52 Both,
54}
55
56#[derive(Debug, Clone, Default)]
61pub struct TriangleCountSpec<'a> {
62 pub rel_types: [Option<&'a str>; 3],
64 pub labels: [Option<&'a str>; 3],
66}
67
68#[derive(Debug, Clone, Default)]
79pub struct PathCountSpec<'a> {
80 pub rel_types: Vec<Option<&'a str>>,
82 pub labels: Vec<Option<&'a str>>,
84 pub vertex_allow: Vec<Option<Vec<NodeId>>>,
92}
93
94#[derive(Debug, Clone, Default)]
105pub struct GroupedDegreeSpec<'a> {
106 pub rel_type: Option<&'a str>,
108 pub group_is_dst: bool,
111 pub group_label: Option<&'a str>,
113 pub counted_label: Option<&'a str>,
115 pub counted_nonnull_prop: Option<&'a str>,
118}
119
120pub(super) fn composite_key(prefix: u32, id: u64) -> [u8; 12] {
122 let mut key = [0u8; 12];
123 key[..4].copy_from_slice(&prefix.to_be_bytes());
124 key[4..].copy_from_slice(&id.to_be_bytes());
125 key
126}
127
128pub(super) const ENCODED_NULL: u8 = 0x00;
130
131const SORT_SIGN_BIT: u64 = 0x8000_0000_0000_0000;
134
135pub(super) const MAX_INDEXED_STRING_LEN: usize = 480;
144
145pub(super) fn encode_property_value(val: &serde_json::Value) -> Option<Vec<u8>> {
157 match val {
158 serde_json::Value::Null => Some(vec![ENCODED_NULL]),
159 serde_json::Value::Bool(false) => Some(vec![0x01]),
160 serde_json::Value::Bool(true) => Some(vec![0x02]),
161 serde_json::Value::Number(num) => {
162 let float_val = num.as_f64()?;
163 let bits = float_val.to_bits();
164 let masked = if (bits & SORT_SIGN_BIT) != 0 {
165 !bits
166 } else {
167 bits ^ SORT_SIGN_BIT
168 };
169 let int_disambig: u64 = if let Some(i) = num.as_i64() {
176 (i as u64) ^ SORT_SIGN_BIT
177 } else if float_val.fract() == 0.0
178 && float_val >= i64::MIN as f64
179 && float_val <= i64::MAX as f64
180 {
181 ((float_val as i64) as u64) ^ SORT_SIGN_BIT
182 } else {
183 0
184 };
185 let mut buf = Vec::with_capacity(17);
186 buf.push(0x03);
187 buf.extend_from_slice(&masked.to_be_bytes());
188 buf.extend_from_slice(&int_disambig.to_be_bytes());
189 Some(buf)
190 }
191 serde_json::Value::String(s) => {
192 if s.len() > MAX_INDEXED_STRING_LEN {
195 return None;
196 }
197 let mut buf = Vec::with_capacity(1 + s.len() + 1);
198 buf.push(0x04);
199 buf.extend_from_slice(s.as_bytes());
200 buf.push(0x00);
201 Some(buf)
202 }
203 _ => None, }
205}
206
207#[allow(dead_code)]
209pub(super) fn decode_property_value(bytes: &[u8]) -> Option<serde_json::Value> {
210 if bytes.is_empty() {
211 return None;
212 }
213 match bytes[0] {
214 0x00 => Some(serde_json::Value::Null),
215 0x01 => Some(serde_json::Value::Bool(false)),
216 0x02 => Some(serde_json::Value::Bool(true)),
217 0x03 => {
218 if bytes.len() < 17 {
220 return None;
221 }
222 let mut int_arr = [0u8; 8];
225 int_arr.copy_from_slice(&bytes[9..17]);
226 let int_val = (u64::from_be_bytes(int_arr) ^ SORT_SIGN_BIT) as i64;
227
228 let mut arr = [0u8; 8];
229 arr.copy_from_slice(&bytes[1..9]);
230 let masked = u64::from_be_bytes(arr);
231 let bits = if (masked & SORT_SIGN_BIT) == 0 {
232 !masked
233 } else {
234 masked ^ SORT_SIGN_BIT
235 };
236 let float_val = f64::from_bits(bits);
237
238 if (int_val as f64) == float_val {
243 Some(serde_json::Value::Number(int_val.into()))
244 } else {
245 serde_json::Number::from_f64(float_val).map(serde_json::Value::Number)
246 }
247 }
248 0x04 => {
249 let str_bytes = if bytes.ends_with(&[0x00]) {
250 &bytes[1..bytes.len() - 1]
251 } else {
252 &bytes[1..]
253 };
254 String::from_utf8(str_bytes.to_vec())
255 .ok()
256 .map(serde_json::Value::String)
257 }
258 _ => None,
259 }
260}
261
262pub(super) fn node_prop_index_key(
264 label_id: LabelId,
265 prop_key_id: PropKeyId,
266 encoded_val: &[u8],
267 node_id: NodeId,
268) -> Vec<u8> {
269 let mut key = Vec::with_capacity(4 + 4 + encoded_val.len() + 8);
270 key.extend_from_slice(&label_id.to_be_bytes());
271 key.extend_from_slice(&prop_key_id.to_be_bytes());
272 key.extend_from_slice(encoded_val);
273 key.extend_from_slice(&node_id.to_be_bytes());
274 key
275}
276
277pub(super) fn edge_prop_index_key(
279 type_id: TypeId,
280 prop_key_id: PropKeyId,
281 encoded_val: &[u8],
282 edge_id: EdgeId,
283) -> Vec<u8> {
284 let mut key = Vec::with_capacity(4 + 4 + encoded_val.len() + 8);
285 key.extend_from_slice(&type_id.to_be_bytes());
286 key.extend_from_slice(&prop_key_id.to_be_bytes());
287 key.extend_from_slice(encoded_val);
288 key.extend_from_slice(&edge_id.to_be_bytes());
289 key
290}
291
292pub(super) fn fts_postings_key(label_id: LabelId, prop_key_id: PropKeyId, term: &str) -> Vec<u8> {
294 let mut key = Vec::with_capacity(8 + term.len());
295 key.extend_from_slice(&label_id.to_be_bytes());
296 key.extend_from_slice(&prop_key_id.to_be_bytes());
297 key.extend_from_slice(term.as_bytes());
298 key
299}
300
301pub(super) fn fts_posting_val(node_id: NodeId, frequency: u32) -> [u8; 12] {
303 let mut val = [0u8; 12];
304 val[0..8].copy_from_slice(&node_id.to_be_bytes());
305 val[8..12].copy_from_slice(&frequency.to_be_bytes());
306 val
307}
308
309pub(super) fn parse_fts_posting_val(bytes: &[u8]) -> Result<(NodeId, u32), Error> {
311 if bytes.len() != 12 {
312 return Err(Error::Corrupt("fts posting value must be 12 bytes"));
313 }
314 let node_id = NodeId::from_be_bytes(
315 bytes[0..8]
316 .try_into()
317 .map_err(|_| Error::Corrupt("fts posting: node_id slice wrong size"))?,
318 );
319 let frequency = u32::from_be_bytes(
320 bytes[8..12]
321 .try_into()
322 .map_err(|_| Error::Corrupt("fts posting: frequency slice wrong size"))?,
323 );
324 Ok((node_id, frequency))
325}
326
327pub(super) fn fts_doc_key(label_id: LabelId, prop_key_id: PropKeyId, node_id: NodeId) -> [u8; 16] {
329 let mut key = [0u8; 16];
330 key[0..4].copy_from_slice(&label_id.to_be_bytes());
331 key[4..8].copy_from_slice(&prop_key_id.to_be_bytes());
332 key[8..16].copy_from_slice(&node_id.to_be_bytes());
333 key
334}
335
336pub(super) fn parse_fts_doc_val(bytes: &[u8]) -> Result<u32, Error> {
338 if bytes.len() != 4 {
339 return Err(Error::Corrupt("fts doc val must be 4 bytes"));
340 }
341 Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
342 Error::Corrupt("fts doc val: slice wrong size")
343 })?))
344}
345
346pub(super) fn fts_stats_n_key(label_id: LabelId, prop_key_id: PropKeyId) -> String {
347 format!("fts_stats:node:l:{label_id}:p:{prop_key_id}:N")
348}
349
350pub(super) fn fts_stats_sum_dl_key(label_id: LabelId, prop_key_id: PropKeyId) -> String {
351 format!("fts_stats:node:l:{label_id}:p:{prop_key_id}:sum_dl")
352}
353
354#[derive(Clone)]
356pub struct Graph {
357 pub(super) storage: Arc<Storage>,
358 pub(super) _write_lock: Arc<ReentrantMutex<()>>,
359 pub(super) csr_cache: Arc<CsrCache>,
360 pub(super) matrices: Arc<parking_lot::RwLock<Option<MatrixSet>>>,
361 pub(super) prop_columns: Arc<crate::columns::ColumnsCache<crate::columns::NodeSource>>,
362 pub(super) edge_columns: Arc<crate::columns::ColumnsCache<crate::columns::EdgeSource>>,
363 pub(super) edge_fanout: Arc<parking_lot::Mutex<Option<crate::graph::stats::EdgeFanout>>>,
367 pub(super) n_threads: Arc<std::sync::atomic::AtomicI32>,
368 pub(crate) extensions: Arc<parking_lot::Mutex<AHashMap<StdTypeId, Box<dyn Any + Send + Sync>>>>,
374}
375
376pub struct ReadTxn<'a> {
378 pub(super) graph: &'a Graph,
379 pub(super) rtxn: heed::RoTxn<'a, heed::WithTls>,
380}
381
382pub struct WriteTxn<'a> {
384 pub(super) graph: &'a Graph,
385 pub(super) wtxn: heed::RwTxn<'a>,
386 pub(super) mutations_count: usize,
387 pub(super) delta: crate::csr::GraphDelta,
390}
391
392impl Graph {
393 pub fn open(path: &Path, map_size_gb: usize) -> Result<Self, Error> {
394 let storage = Storage::open(path, map_size_gb)?;
395 let _ = std::fs::remove_file(path.join("csr_snapshot.bin"));
398 let initial = CsrSnapshot::build(&storage)?;
399 let storage = Arc::new(storage);
400 let csr_cache = Arc::new(CsrCache::new(initial));
401 let matrices = {
402 let initial_snap = csr_cache.snapshot.load();
403 let m = MatrixSet::materialize(&initial_snap, 0)?;
404 Arc::new(parking_lot::RwLock::new(Some(m)))
405 };
406 Ok(Self {
407 storage,
408 _write_lock: Arc::new(ReentrantMutex::new(())),
409 csr_cache,
410 matrices,
411 prop_columns: Arc::new(crate::columns::ColumnsCache::default()),
412 edge_columns: Arc::new(crate::columns::ColumnsCache::default()),
413 edge_fanout: Arc::new(parking_lot::Mutex::new(None)),
414 n_threads: Arc::new(std::sync::atomic::AtomicI32::new(0)),
415 extensions: Arc::new(parking_lot::Mutex::new(AHashMap::new())),
416 })
417 }
418
419 pub fn set_thread_count(&self, n: i32) -> Result<(), Error> {
422 self.n_threads
423 .store(n, std::sync::atomic::Ordering::Release);
424 issundb_graphblas::set_global_threads(n).map_err(|e| Error::GraphBLAS(e.to_string()))?;
425 Ok(())
426 }
427
428 pub fn node_prop_json(
434 &self,
435 id: NodeId,
436 prop: &str,
437 ) -> Result<Option<serde_json::Value>, Error> {
438 self.prop_columns.with_fresh(&self.storage, |cols| {
439 cols.id_to_dense.get(&id).map(|&d| {
440 cols.cols
441 .get(prop)
442 .and_then(|c| c.get_json_opt(d as usize))
443 .unwrap_or(serde_json::Value::Null)
444 })
445 })
446 }
447
448 pub fn node_props_json_table(
454 &self,
455 ids: &[NodeId],
456 props: &[&str],
457 ) -> Result<Vec<Vec<serde_json::Value>>, Error> {
458 self.prop_columns
459 .with_fresh(&self.storage, |cols| cols.props_table(ids, props))?
460 }
461
462 pub fn node_prop_json_column(
468 &self,
469 ids: &[NodeId],
470 prop: &str,
471 ) -> Result<Vec<serde_json::Value>, Error> {
472 self.prop_columns
473 .with_fresh(&self.storage, |cols| cols.prop_column(ids, prop))?
474 }
475
476 pub fn node_prop_group_codes(
483 &self,
484 ids: &[NodeId],
485 prop: &str,
486 ) -> Result<(Vec<u32>, Vec<serde_json::Value>), Error> {
487 self.prop_columns
488 .with_fresh(&self.storage, |cols| cols.group_codes(ids, prop))?
489 }
490
491 pub fn edge_prop_json(
507 &self,
508 id: EdgeId,
509 prop: &str,
510 ) -> Result<Option<serde_json::Value>, Error> {
511 self.edge_columns.with_fresh(&self.storage, |cols| {
512 cols.id_to_dense.get(&id).map(|&d| {
513 cols.cols
514 .get(prop)
515 .and_then(|c| c.get_json_opt(d as usize))
516 .unwrap_or(serde_json::Value::Null)
517 })
518 })
519 }
520
521 pub fn edge_props_json_table(
523 &self,
524 ids: &[EdgeId],
525 props: &[&str],
526 ) -> Result<Vec<Vec<serde_json::Value>>, Error> {
527 self.edge_columns
528 .with_fresh(&self.storage, |cols| cols.props_table(ids, props))?
529 }
530
531 pub fn edge_prop_json_column(
533 &self,
534 ids: &[EdgeId],
535 prop: &str,
536 ) -> Result<Vec<serde_json::Value>, Error> {
537 self.edge_columns
538 .with_fresh(&self.storage, |cols| cols.prop_column(ids, prop))?
539 }
540
541 pub fn edge_prop_group_codes(
544 &self,
545 ids: &[EdgeId],
546 prop: &str,
547 ) -> Result<(Vec<u32>, Vec<serde_json::Value>), Error> {
548 self.edge_columns
549 .with_fresh(&self.storage, |cols| cols.group_codes(ids, prop))?
550 }
551
552 pub fn node_prop_min_max(
556 &self,
557 prop: &str,
558 ) -> Result<Option<(serde_json::Value, serde_json::Value)>, Error> {
559 self.prop_columns.with_fresh_mut(&self.storage, |cols| {
560 cols.prop_stats(prop)
561 .map(|s| (s.min.clone(), s.max.clone()))
562 })
563 }
564
565 pub fn estimate_range_selectivity(
569 &self,
570 prop: &str,
571 lower: Option<&serde_json::Value>,
572 upper: Option<&serde_json::Value>,
573 ) -> Result<Option<f64>, Error> {
574 self.prop_columns.with_fresh_mut(&self.storage, |cols| {
575 cols.prop_stats(prop)
576 .map(|s| s.histogram.estimate_range_selectivity(lower, upper))
577 })
578 }
579
580 pub fn estimate_equality_selectivity(
584 &self,
585 prop: &str,
586 val: &serde_json::Value,
587 ) -> Result<Option<f64>, Error> {
588 self.prop_columns.with_fresh_mut(&self.storage, |cols| {
589 cols.prop_stats(prop).map(|s| s.equality_selectivity(val))
590 })
591 }
592
593 pub fn set_extension<T: Any + Send + Sync>(&self, val: Arc<T>) {
596 self.extensions
597 .lock()
598 .insert(StdTypeId::of::<T>(), Box::new(val));
599 }
600
601 pub fn get_extension<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
603 self.extensions
604 .lock()
605 .get(&StdTypeId::of::<T>())
606 .and_then(|b| b.downcast_ref::<Arc<T>>())
607 .cloned()
608 }
609
610 pub fn get_or_init_extension_with<T, E, F>(&self, init: F) -> Result<Arc<T>, E>
619 where
620 T: Any + Send + Sync,
621 F: FnOnce() -> Result<Arc<T>, E>,
622 {
623 if let Some(existing) = self.get_extension::<T>() {
624 return Ok(existing);
625 }
626 let value = init()?;
627 let mut ext = self.extensions.lock();
628 if let Some(existing) = ext
631 .get(&StdTypeId::of::<T>())
632 .and_then(|b| b.downcast_ref::<Arc<T>>())
633 {
634 return Ok(existing.clone());
635 }
636 ext.insert(StdTypeId::of::<T>(), Box::new(value.clone()));
637 Ok(value)
638 }
639
640 pub fn view<F, T>(&self, f: F) -> Result<T, Error>
642 where
643 F: FnOnce(&ReadTxn) -> Result<T, Error>,
644 {
645 let rtxn = self.storage.env.read_txn()?;
646 let txn = ReadTxn { graph: self, rtxn };
647 f(&txn)
648 }
649
650 pub fn update<F, T>(&self, f: F) -> Result<T, Error>
652 where
653 F: FnOnce(&mut WriteTxn) -> Result<T, Error>,
654 {
655 let _guard = self._write_lock.lock();
656 let wtxn = self.storage.env.write_txn()?;
657 let mut txn = WriteTxn {
658 graph: self,
659 wtxn,
660 mutations_count: 0,
661 delta: crate::csr::GraphDelta::default(),
662 };
663 match f(&mut txn) {
664 Ok(val) => {
665 let mutations_count = txn.mutations_count;
666 let delta = std::mem::take(&mut txn.delta);
667 txn.wtxn.commit()?;
668 if delta.force_full {
669 self.prop_columns.record_force_full();
670 } else {
671 self.prop_columns.record_touched_many(&delta.added_nodes);
672 self.prop_columns.record_touched_many(&delta.updated_nodes);
673 }
674 if delta.force_full || !delta.removed_edges.is_empty() {
678 self.edge_columns.record_force_full();
679 } else {
680 self.edge_columns.record_touched_many(&delta.added_edge_ids);
681 }
682 self.csr_cache.record_batch(delta);
683 if mutations_count > 0 {
684 self.maybe_spawn_rebuild_n(mutations_count);
685 }
686 Ok(val)
687 }
688 Err(err) => {
689 txn.wtxn.abort();
690 Err(err)
691 }
692 }
693 }
694
695 pub fn with_write_lock<F, R>(&self, f: F) -> R
699 where
700 F: FnOnce() -> R,
701 {
702 let _guard = self._write_lock.lock();
703 f()
704 }
705
706 #[instrument(skip(self))]
710 pub fn rebuild_csr(&self) -> Result<(), Error> {
711 let built_gen = self.csr_cache.current_gen();
714 self.csr_cache.clear_delta();
718 let snap = CsrSnapshot::build(&self.storage)?;
719 let m = MatrixSet::materialize(
720 &snap,
721 self.n_threads.load(std::sync::atomic::Ordering::Acquire),
722 )?;
723 *self.matrices.write() = Some(m);
724 self.csr_cache.install_full(snap, built_gen);
725 Ok(())
726 }
727
728 pub fn backup(&self, destination: &Path) -> Result<(), Error> {
737 self.storage
738 .env
739 .copy_to_path(destination, heed::CompactionOption::Disabled)
740 .map(|_| ())
741 .map_err(Error::Storage)
742 }
743
744 pub fn backup_compact(&self, destination: &Path) -> Result<(), Error> {
749 self.storage
750 .env
751 .copy_to_path(destination, heed::CompactionOption::Enabled)
752 .map(|_| ())
753 .map_err(Error::Storage)
754 }
755
756 pub fn restore(snapshot_file: &Path, dst_dir: &Path) -> Result<(), Error> {
763 std::fs::create_dir_all(dst_dir)?;
764 let dst_file = dst_dir.join("data.mdb");
765 std::fs::copy(snapshot_file, &dst_file)?;
766 Ok(())
767 }
768}
769
770#[cfg(test)]
771mod extension_tests {
772 use std::sync::Arc;
773
774 use tempfile::TempDir;
775
776 use super::Graph;
777
778 fn open_tmp() -> (TempDir, Graph) {
779 let dir = TempDir::new().unwrap();
780 let g = Graph::open(dir.path(), 1).unwrap();
781 (dir, g)
782 }
783
784 #[test]
788 fn extension_roundtrip_by_type() {
789 let (_dir, g) = open_tmp();
790 assert!(g.get_extension::<String>().is_none());
791
792 g.set_extension(Arc::new(String::from("cache")));
793 let got = g.get_extension::<String>().expect("extension must exist");
794 assert_eq!(*got, "cache");
795 assert!(g.get_extension::<u64>().is_none(), "distinct type slot");
796
797 g.set_extension(Arc::new(String::from("replaced")));
798 assert_eq!(*g.get_extension::<String>().unwrap(), "replaced");
799 }
800
801 #[test]
804 fn get_or_init_extension_initializes_once() {
805 let (_dir, g) = open_tmp();
806
807 let v1 = g
808 .get_or_init_extension_with::<u64, std::convert::Infallible, _>(|| Ok(Arc::new(7)))
809 .unwrap();
810 assert_eq!(*v1, 7);
811
812 let v2 = g
813 .get_or_init_extension_with::<u64, std::convert::Infallible, _>(|| Ok(Arc::new(9)))
814 .unwrap();
815 assert_eq!(*v2, 7, "second init must not replace the stored value");
816 }
817
818 #[test]
820 fn get_or_init_extension_propagates_init_error() {
821 let (_dir, g) = open_tmp();
822
823 let err = g
824 .get_or_init_extension_with::<u64, &str, _>(|| Err("init failed"))
825 .unwrap_err();
826 assert_eq!(err, "init failed");
827 assert!(g.get_extension::<u64>().is_none());
828
829 let v = g
830 .get_or_init_extension_with::<u64, &str, _>(|| Ok(Arc::new(7)))
831 .unwrap();
832 assert_eq!(*v, 7);
833 }
834}
835
836#[cfg(test)]
837mod encode_tests {
838 use serde_json::json;
839
840 use super::{MAX_INDEXED_STRING_LEN, decode_property_value, encode_property_value};
841
842 #[test]
845 fn over_long_strings_are_not_indexed() {
846 let at_limit = json!("a".repeat(MAX_INDEXED_STRING_LEN));
847 let encoded = encode_property_value(&at_limit).expect("at-limit string indexes");
848 assert_eq!(decode_property_value(&encoded), Some(at_limit));
849
850 let too_long = json!("a".repeat(MAX_INDEXED_STRING_LEN + 1));
851 assert_eq!(
852 encode_property_value(&too_long),
853 None,
854 "a string over the bound must not be indexed",
855 );
856 }
857
858 #[test]
862 fn large_integers_do_not_collide() {
863 let a = encode_property_value(&json!(9_007_199_254_740_992_i64)).unwrap(); let b = encode_property_value(&json!(9_007_199_254_740_993_i64)).unwrap(); assert_ne!(a, b, "distinct large integers must encode distinctly");
866 }
867
868 #[test]
871 fn integer_and_equal_float_unify() {
872 assert_eq!(
873 encode_property_value(&json!(30)).unwrap(),
874 encode_property_value(&json!(30.0)).unwrap(),
875 );
876 assert_eq!(
877 encode_property_value(&json!(0)).unwrap(),
878 encode_property_value(&json!(0.0)).unwrap(),
879 );
880 }
881
882 #[test]
885 fn numeric_encoding_is_fixed_length() {
886 for v in [
887 json!(1),
888 json!(-1),
889 json!(0),
890 json!(i64::MAX),
891 json!(i64::MIN),
892 json!(3.5),
893 json!(-2.5e10),
894 ] {
895 assert_eq!(encode_property_value(&v).unwrap().len(), 17, "value {v}");
896 }
897 }
898
899 #[test]
902 fn numeric_ordering_preserved() {
903 let ascending: Vec<i64> = vec![
904 i64::MIN,
905 -1_000,
906 -1,
907 0,
908 1,
909 1_000,
910 1 << 53,
911 (1 << 53) + 1,
912 i64::MAX,
913 ];
914 let encoded: Vec<Vec<u8>> = ascending
915 .iter()
916 .map(|v| encode_property_value(&json!(v)).unwrap())
917 .collect();
918 let mut sorted = encoded.clone();
919 sorted.sort();
920 assert_eq!(encoded, sorted, "encodings must sort in numeric order");
921 }
922
923 #[test]
925 fn decode_round_trips_large_integer() {
926 for v in [
927 json!(0),
928 json!(-1),
929 json!(9_007_199_254_740_993_i64),
930 json!(i64::MAX),
931 ] {
932 let enc = encode_property_value(&v).unwrap();
933 assert_eq!(decode_property_value(&enc), Some(v.clone()), "value {v}");
934 }
935 }
936}