1use core_storage::{list_tokens, Value, ValueKey};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeSet;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub struct RuleDef {
7 pub name: String,
8 pub src_label: String,
9 pub dst_label: String,
10 pub predicate: Predicate,
11 pub edge_type: String,
12 pub weight_prop: Option<String>,
13 pub max_edges: Option<u64>,
21 #[serde(default)]
37 pub approximate: bool,
38 #[serde(default)]
56 pub via_label: Option<String>,
57 #[serde(default)]
61 pub via_edge: Option<String>,
62 #[serde(default)]
67 pub via_dir: Option<core_storage::Direction>,
68 #[serde(default)]
80 pub namespace: Option<String>,
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub enum Predicate {
99 KeyMatch {
100 field: String,
101 },
102 FieldEqual {
103 field: String,
104 },
105 Overlap {
106 field: String,
107 min: f64,
108 },
109 All(Vec<Predicate>),
110 NumericWithin {
112 field: String,
113 tolerance: f64,
114 },
115 GeoRadius {
116 field: String,
117 km: f64,
118 },
119 VectorSimilar {
120 field: String,
121 min: f64,
122 },
123 Any(Vec<Predicate>),
127}
128
129pub struct NodeView<'a> {
130 pub key: &'a str,
131 pub props: &'a dyn Fn(&str) -> Option<Value>,
132}
133
134impl RuleDef {
135 pub fn validate(&self) -> Result<(), String> {
136 for (what, s) in [
137 ("name", &self.name),
138 ("src_label", &self.src_label),
139 ("dst_label", &self.dst_label),
140 ("edge_type", &self.edge_type),
141 ] {
142 if s.is_empty() {
143 return Err(format!("{what} must not be empty"));
144 }
145 }
146 validate_pred(&self.predicate)?;
147 let depth = predicate_nesting_depth(&self.predicate);
148 if depth > MAX_PREDICATE_NESTING_DEPTH {
149 return Err(format!(
150 "predicate nesting depth {depth} exceeds cap of \
151 {MAX_PREDICATE_NESTING_DEPTH}"
152 ));
153 }
154 if self.approximate && !predicate_is_vector_similar_rooted(&self.predicate) {
155 return Err(
156 "approximate=true requires a VectorSimilar-rooted predicate \
157 (VectorSimilar, or All whose first element is VectorSimilar)"
158 .into(),
159 );
160 }
161 if self.via_label.is_some() && self.approximate {
162 return Err("via-hop rules do not support approximate: true".into());
163 }
164 match (&self.via_label, &self.via_edge) {
166 (Some(_), None) | (None, Some(_)) => {
167 return Err("via_label and via_edge must both be set or both absent".into());
168 }
169 (Some(l), Some(e)) => {
170 if l.is_empty() {
171 return Err("via_label must not be empty".into());
172 }
173 if e.is_empty() {
174 return Err("via_edge must not be empty".into());
175 }
176 }
177 (None, None) => {}
178 }
179 if let Some(ns) = &self.namespace {
180 if !core_storage::valid_namespace(ns) {
181 return Err(format!(
182 "namespace {ns:?} is not a valid namespace name — 1 to {} characters \
183 of [A-Za-z0-9_.-]",
184 core_storage::NS_MAX_LEN
185 ));
186 }
187 }
188 Ok(())
189 }
190
191 pub fn sees_namespace(&self, namespace: &str) -> bool {
196 match &self.namespace {
197 None => true,
198 Some(ns) => ns == namespace,
199 }
200 }
201
202 pub fn watched_fields(&self) -> BTreeSet<String> {
203 let mut out = BTreeSet::new();
204 collect_fields(&self.predicate, &mut out);
205 out
206 }
207}
208
209pub const MAX_PREDICATE_NESTING_DEPTH: usize = 4;
216
217pub const DEFAULT_SCORED_TOP_K: u64 = 32;
219
220pub const DEFAULT_KEYMATCH_TOP_K: u64 = MAX_KEYMATCH_LIST as u64;
227
228pub const MAX_KEYMATCH_LIST: usize = 512;
237
238pub fn is_keymatch_rooted(p: &Predicate) -> bool {
241 match p {
242 Predicate::KeyMatch { .. } => true,
243 Predicate::All(parts) => !parts.is_empty() && is_keymatch_rooted(&parts[0]),
244 Predicate::Any(_) => false,
245 _ => false,
246 }
247}
248
249pub fn predicate_contains_keymatch(p: &Predicate) -> bool {
260 match p {
261 Predicate::KeyMatch { .. } => true,
262 Predicate::All(parts) | Predicate::Any(parts) => {
263 parts.iter().any(predicate_contains_keymatch)
264 }
265 _ => false,
266 }
267}
268
269pub fn default_max_edges(predicate: &Predicate) -> u64 {
271 if is_keymatch_rooted(predicate) {
272 DEFAULT_KEYMATCH_TOP_K
273 } else {
274 DEFAULT_SCORED_TOP_K
275 }
276}
277
278pub fn predicate_is_vector_similar_rooted(p: &Predicate) -> bool {
282 match p {
283 Predicate::VectorSimilar { .. } => true,
284 Predicate::All(parts) => {
285 !parts.is_empty() && matches!(parts[0], Predicate::VectorSimilar { .. })
286 }
287 Predicate::Any(_) => false,
288 _ => false,
289 }
290}
291
292fn predicate_nesting_depth(p: &Predicate) -> usize {
298 match p {
299 Predicate::All(parts) | Predicate::Any(parts) => {
300 1 + parts.iter().map(predicate_nesting_depth).max().unwrap_or(0)
301 }
302 _ => 0,
303 }
304}
305
306fn validate_pred(p: &Predicate) -> Result<(), String> {
307 match p {
308 Predicate::KeyMatch { field } | Predicate::FieldEqual { field } => {
309 if field.is_empty() {
310 Err("field must not be empty".into())
311 } else {
312 Ok(())
313 }
314 }
315 Predicate::Overlap { field, min } => {
316 if field.is_empty() {
317 Err("field must not be empty".into())
318 } else if !(*min > 0.0 && *min <= 1.0) {
319 Err(format!("overlap min must be in (0,1], got {min}"))
320 } else {
321 Ok(())
322 }
323 }
324 Predicate::NumericWithin { field, tolerance } => {
325 if field.is_empty() {
326 Err("field must not be empty".into())
327 } else if !(tolerance.is_finite() && *tolerance >= 0.0) {
328 Err(format!(
329 "numeric_within tolerance must be finite and >= 0, got {tolerance}"
330 ))
331 } else {
332 Ok(())
333 }
334 }
335 Predicate::GeoRadius { field, km } => {
336 if field.is_empty() {
337 Err("field must not be empty".into())
338 } else if !(km.is_finite() && *km > 0.0) {
339 Err(format!("geo_radius km must be finite and > 0, got {km}"))
340 } else {
341 Ok(())
342 }
343 }
344 Predicate::VectorSimilar { field, min } => {
345 if field.is_empty() {
346 Err("field must not be empty".into())
347 } else if !(*min > 0.0 && *min <= 1.0) {
348 Err(format!("vector_similar min must be in (0,1], got {min}"))
349 } else {
350 Ok(())
351 }
352 }
353 Predicate::All(parts) => {
354 if parts.is_empty() {
355 return Err("all() must have at least one predicate".into());
356 }
357 parts.iter().try_for_each(validate_pred)
358 }
359 Predicate::Any(parts) => {
360 if parts.is_empty() {
361 return Err("any() must have at least one predicate".into());
362 }
363 parts.iter().try_for_each(validate_pred)
364 }
365 }
366}
367
368fn collect_fields(p: &Predicate, out: &mut BTreeSet<String>) {
369 match p {
370 Predicate::KeyMatch { field }
371 | Predicate::FieldEqual { field }
372 | Predicate::Overlap { field, .. }
373 | Predicate::NumericWithin { field, .. }
374 | Predicate::GeoRadius { field, .. }
375 | Predicate::VectorSimilar { field, .. } => {
376 out.insert(field.clone());
377 }
378 Predicate::All(parts) | Predicate::Any(parts) => {
379 parts.iter().for_each(|q| collect_fields(q, out))
380 }
381 }
382}
383
384pub fn evaluate(pred: &Predicate, src: &NodeView, dst: &NodeView) -> Option<f64> {
385 match pred {
386 Predicate::KeyMatch { field } => match (src.props)(field)? {
387 Value::Str(s) if s == dst.key => Some(1.0),
388 Value::List(items) => items
393 .iter()
394 .take(MAX_KEYMATCH_LIST)
395 .any(|v| matches!(v, Value::Str(s) if s == dst.key))
396 .then_some(1.0),
397 _ => None,
398 },
399 Predicate::FieldEqual { field } => {
400 let a = ValueKey::from_value(&(src.props)(field)?)?;
401 let b = ValueKey::from_value(&(dst.props)(field)?)?;
402 (a == b).then_some(1.0)
403 }
404 Predicate::Overlap { field, min } => {
405 let a = list_tokens(&(src.props)(field)?)?;
406 let b = list_tokens(&(dst.props)(field)?)?;
407 let inter = a.intersection(&b).count();
408 let union = a.union(&b).count();
409 if union == 0 || inter == 0 {
410 return None;
411 }
412 let j = inter as f64 / union as f64;
413 (j >= *min).then_some(j)
414 }
415 Predicate::All(parts) => {
416 if parts.is_empty() {
418 return None;
419 }
420 let mut score = f64::INFINITY;
421 for part in parts {
422 score = score.min(evaluate(part, src, dst)?);
423 }
424 Some(score)
425 }
426 Predicate::Any(parts) => {
427 let mut best: Option<f64> = None;
431 for part in parts {
432 if let Some(s) = evaluate(part, src, dst) {
433 best = Some(match best {
434 None => s,
435 Some(prev) => prev.max(s),
436 });
437 }
438 }
439 best
440 }
441 Predicate::NumericWithin { field, tolerance } => {
442 if !tolerance.is_finite() || *tolerance < 0.0 {
446 return None;
447 }
448 let a = as_finite_f64(&(src.props)(field)?)?;
449 let b = as_finite_f64(&(dst.props)(field)?)?;
450 let delta = (a - b).abs();
451 if *tolerance == 0.0 {
452 return (delta == 0.0).then_some(1.0);
453 }
454 (delta <= *tolerance).then_some(1.0 - delta / *tolerance)
455 }
456 Predicate::GeoRadius { field, km } => {
457 if !km.is_finite() || *km <= 0.0 {
458 return None;
459 }
460 let (alat, alon) = as_latlon(&(src.props)(field)?)?;
461 let (blat, blon) = as_latlon(&(dst.props)(field)?)?;
462 let d = haversine_km(alat, alon, blat, blon);
463 if !d.is_finite() {
464 return None;
465 }
466 (d <= *km).then_some(1.0 - d / *km)
467 }
468 Predicate::VectorSimilar { field, min } => {
469 let a = as_numeric_list(&(src.props)(field)?)?;
470 let b = as_numeric_list(&(dst.props)(field)?)?;
471 if a.len() != b.len() {
472 return None;
473 }
474 let cos = cosine(&a, &b)?.min(1.0);
475 (cos >= *min).then_some(cos)
476 }
477 }
478}
479
480fn as_finite_f64(v: &Value) -> Option<f64> {
481 match v {
482 Value::Int(i) => Some(*i as f64),
483 Value::Float(f) if f.is_finite() => Some(*f),
484 _ => None,
485 }
486}
487
488fn as_latlon(v: &Value) -> Option<(f64, f64)> {
489 let Value::List(items) = v else {
490 return None;
491 };
492 if items.len() != 2 {
493 return None;
494 }
495 let lat = as_finite_f64(&items[0])?;
496 let lon = as_finite_f64(&items[1])?;
497 if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon) {
498 Some((lat, lon))
499 } else {
500 None
501 }
502}
503
504fn as_numeric_list(v: &Value) -> Option<Vec<f64>> {
505 let Value::List(items) = v else {
506 return None;
507 };
508 if items.is_empty() {
509 return None;
510 }
511 items.iter().map(as_finite_f64).collect()
512}
513
514const EARTH_RADIUS_KM: f64 = 6371.0088;
516
517fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
518 let phi1 = lat1.to_radians();
519 let phi2 = lat2.to_radians();
520 let dphi = (lat2 - lat1).to_radians();
521 let dlam = (lon2 - lon1).to_radians();
522 let a = ((dphi / 2.0).sin().powi(2) + phi1.cos() * phi2.cos() * (dlam / 2.0).sin().powi(2))
523 .clamp(0.0, 1.0);
524 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
525 EARTH_RADIUS_KM * c
526}
527
528fn cosine(a: &[f64], b: &[f64]) -> Option<f64> {
529 let mut dot = 0.0;
530 let mut na2 = 0.0;
531 let mut nb2 = 0.0;
532 for (x, y) in a.iter().zip(b.iter()) {
533 dot += *x * *y;
534 na2 += *x * *x;
535 nb2 += *y * *y;
536 }
537 let na = na2.sqrt();
538 let nb = nb2.sqrt();
539 if !(na > 0.0 && nb > 0.0) {
540 return None;
541 }
542 let cos = dot / (na * nb);
543 cos.is_finite().then_some(cos)
544}
545
546pub fn cosine_early_exit(
582 a: &[f64],
583 b: &[f64],
584 ckpts_a: &[f64; 8],
585 ckpts_b: &[f64; 8],
586 norm_a: f64,
587 norm_b: f64,
588 min: f64,
589) -> Option<f64> {
590 let dim = a.len();
591 if dim == 0 || dim != b.len() {
592 return None; }
594 let denom = norm_a * norm_b;
595 if !denom.is_finite() || denom == 0.0 {
596 return None;
597 }
598
599 let eps = dim as f64 * f64::EPSILON * 4.0;
605 let mut dot = 0.0f64;
606
607 for ci in 0..8usize {
608 let chunk_start = ci * dim / 8;
609 let chunk_end = if ci < 7 { (ci + 1) * dim / 8 } else { dim };
610 for k in chunk_start..chunk_end {
611 dot += a[k] * b[k];
612 }
613 if ci < 7 {
617 let bound = ckpts_a[ci + 1] * ckpts_b[ci + 1];
618 let cos_max = (dot + bound) / denom;
619 if cos_max.is_finite() && cos_max < min - eps {
620 return None;
621 }
622 }
623 }
624
625 let cos = (dot / denom).min(1.0);
627 if cos.is_finite() && cos >= min {
628 Some(cos)
629 } else {
630 None
631 }
632}
633
634#[derive(serde::Serialize, serde::Deserialize)]
648struct LegacyRuleDefNoVia {
649 name: String,
650 src_label: String,
651 dst_label: String,
652 predicate: Predicate,
653 edge_type: String,
654 weight_prop: Option<String>,
655 max_edges: Option<u64>,
656 approximate: bool,
657}
658
659#[derive(serde::Serialize, serde::Deserialize)]
666struct LegacyRuleDefNoNamespace {
667 name: String,
668 src_label: String,
669 dst_label: String,
670 predicate: Predicate,
671 edge_type: String,
672 weight_prop: Option<String>,
673 max_edges: Option<u64>,
674 approximate: bool,
675 via_label: Option<String>,
676 via_edge: Option<String>,
677 via_dir: Option<core_storage::Direction>,
678}
679
680pub fn decode_rule_def(bytes: &[u8]) -> Result<RuleDef, String> {
699 use bincode::Options as _;
700 let opts = bincode::options()
706 .with_fixint_encoding()
707 .with_no_limit()
708 .reject_trailing_bytes();
709
710 match opts.deserialize::<RuleDef>(bytes) {
712 Ok(def) => Ok(def),
713 Err(current_err) => {
714 if let Ok(prev) = opts.deserialize::<LegacyRuleDefNoNamespace>(bytes) {
718 return Ok(RuleDef {
719 name: prev.name,
720 src_label: prev.src_label,
721 dst_label: prev.dst_label,
722 predicate: prev.predicate,
723 edge_type: prev.edge_type,
724 weight_prop: prev.weight_prop,
725 max_edges: prev.max_edges,
726 approximate: prev.approximate,
727 via_label: prev.via_label,
728 via_edge: prev.via_edge,
729 via_dir: prev.via_dir,
730 namespace: None,
731 });
732 }
733 match opts.deserialize::<LegacyRuleDefNoVia>(bytes) {
735 Ok(legacy) => Ok(RuleDef {
736 name: legacy.name,
737 src_label: legacy.src_label,
738 dst_label: legacy.dst_label,
739 predicate: legacy.predicate,
740 edge_type: legacy.edge_type,
741 weight_prop: legacy.weight_prop,
742 max_edges: legacy.max_edges,
743 approximate: legacy.approximate,
744 via_label: None,
745 via_edge: None,
746 via_dir: None,
747 namespace: None,
748 }),
749 Err(legacy_err) => Err(format!(
750 "corrupt rule_def — current-shape: {current_err}; \
751 legacy-shape (pre-0.1.2 no-via): {legacy_err}"
752 )),
753 }
754 }
755 }
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761 use core_storage::Value;
762 use std::collections::HashMap;
763
764 macro_rules! eval {
770 ($p:expr, ($sk:expr, $sm:ident) => ($dk:expr, $dm:ident)) => {{
771 let sp = |f: &str| $sm.get(f).cloned();
772 let dp = |f: &str| $dm.get(f).cloned();
773 evaluate(
774 $p,
775 &NodeView {
776 key: $sk,
777 props: &sp,
778 },
779 &NodeView {
780 key: $dk,
781 props: &dp,
782 },
783 )
784 }};
785 }
786
787 #[test]
788 fn key_match_links_fk_to_key() {
789 let s: HashMap<_, _> = [("cid".to_string(), Value::Str("c1".into()))].into();
790 let d: HashMap<String, Value> = HashMap::new();
791 let p = Predicate::KeyMatch {
792 field: "cid".into(),
793 };
794 assert_eq!(eval!(&p, ("t1", s) => ("c1", d)), Some(1.0));
795 assert_eq!(eval!(&p, ("t1", s) => ("c2", d)), None);
796 assert_eq!(eval!(&p, ("t1", d) => ("c1", d)), None); }
798
799 #[test]
800 fn field_equal_needs_both_scalars_equal() {
801 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
802 let b = a.clone();
803 let c: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
804 let p = Predicate::FieldEqual {
805 field: "ind".into(),
806 };
807 assert_eq!(eval!(&p, ("a", a) => ("b", b)), Some(1.0));
808 assert_eq!(eval!(&p, ("a", a) => ("c", c)), None);
809 }
810
811 #[test]
812 fn overlap_is_jaccard_with_threshold() {
813 let mk =
814 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
815 let a: HashMap<_, _> = [("tags".to_string(), mk(&["x", "y"]))].into();
816 let b: HashMap<_, _> = [("tags".to_string(), mk(&["y", "z"]))].into();
817 let p = Predicate::Overlap {
818 field: "tags".into(),
819 min: 0.3,
820 };
821 let score = eval!(&p, ("a", a) => ("b", b)).unwrap();
823 assert!((score - 1.0 / 3.0).abs() < 1e-9);
824 let strict = Predicate::Overlap {
825 field: "tags".into(),
826 min: 0.5,
827 };
828 assert_eq!(eval!(&strict, ("a", a) => ("b", b)), None);
829 let e: HashMap<_, _> = [("tags".to_string(), mk(&[]))].into();
831 assert_eq!(eval!(&p, ("a", e) => ("b", b)), None);
832 }
833
834 #[test]
835 fn all_takes_min_score_and_requires_every_part() {
836 let mk =
837 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
838 let a: HashMap<_, _> = [
839 ("ind".to_string(), Value::Str("arch".into())),
840 ("tags".to_string(), mk(&["x", "y"])),
841 ]
842 .into();
843 let b: HashMap<_, _> = [
844 ("ind".to_string(), Value::Str("arch".into())),
845 ("tags".to_string(), mk(&["y"])),
846 ]
847 .into();
848 let p = Predicate::All(vec![
849 Predicate::FieldEqual {
850 field: "ind".into(),
851 },
852 Predicate::Overlap {
853 field: "tags".into(),
854 min: 0.4,
855 },
856 ]);
857 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
858 assert!((s - 0.5).abs() < 1e-9); }
860
861 #[test]
862 fn validation_rejects_bad_rules_and_collects_watched_fields() {
863 let ok = RuleDef {
864 name: "r".into(),
865 src_label: "A".into(),
866 dst_label: "B".into(),
867 predicate: Predicate::All(vec![
868 Predicate::KeyMatch { field: "fk".into() },
869 Predicate::Overlap {
870 field: "tags".into(),
871 min: 0.5,
872 },
873 ]),
874 edge_type: "E".into(),
875 weight_prop: Some("score".into()),
876 max_edges: None,
877 approximate: false,
878 via_label: None,
879 via_edge: None,
880 via_dir: None,
881 namespace: None,
882 };
883 assert!(ok.validate().is_ok());
884 assert_eq!(
885 ok.watched_fields().into_iter().collect::<Vec<_>>(),
886 vec!["fk".to_string(), "tags".to_string()]
887 );
888 let mut bad = ok.clone();
889 bad.predicate = Predicate::Overlap {
890 field: "t".into(),
891 min: 0.0,
892 };
893 assert!(bad.validate().is_err()); let mut bad2 = ok.clone();
895 bad2.edge_type = String::new();
896 assert!(bad2.validate().is_err());
897 let mut bad3 = ok;
898 bad3.predicate = Predicate::All(vec![]);
899 assert!(bad3.validate().is_err());
900 }
901
902 #[test]
903 fn evaluate_empty_all_returns_none() {
904 let empty: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
905 let sp = |f: &str| empty.get(f).cloned();
906 let dp = |f: &str| empty.get(f).cloned();
907 let src = NodeView {
908 key: "a",
909 props: &sp,
910 };
911 let dst = NodeView {
912 key: "b",
913 props: &dp,
914 };
915 assert_eq!(evaluate(&Predicate::All(vec![]), &src, &dst), None);
916 }
917
918 #[test]
919 fn numeric_within_int_float_cross_type() {
920 let a: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
921 let b: HashMap<_, _> = [("year".to_string(), Value::Float(2000.0))].into();
922 let tight = Predicate::NumericWithin {
923 field: "year".into(),
924 tolerance: 2.0,
925 };
926 assert_eq!(eval!(&tight, ("a", a) => ("b", b)), Some(0.0));
928 let loose = Predicate::NumericWithin {
929 field: "year".into(),
930 tolerance: 3.0,
931 };
932 let score = eval!(&loose, ("a", a) => ("b", b)).unwrap();
933 assert!((score - 1.0 / 3.0).abs() < 1e-9);
934 }
935
936 #[test]
937 fn numeric_within_missing_or_non_numeric_is_none() {
938 let num: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
939 let missing: HashMap<String, Value> = HashMap::new();
940 let text: HashMap<_, _> = [("year".to_string(), Value::Str("1998".into()))].into();
941 let p = Predicate::NumericWithin {
942 field: "year".into(),
943 tolerance: 2.0,
944 };
945 assert_eq!(eval!(&p, ("a", num) => ("b", missing)), None);
946 assert_eq!(eval!(&p, ("a", missing) => ("b", num)), None);
947 assert_eq!(eval!(&p, ("a", num) => ("b", text)), None);
948 }
949
950 #[test]
951 fn numeric_within_tol_zero_requires_exact() {
952 let a: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
953 let same: HashMap<_, _> = [("year".to_string(), Value::Float(1998.0))].into();
954 let other: HashMap<_, _> = [("year".to_string(), Value::Int(1999))].into();
955 let p = Predicate::NumericWithin {
956 field: "year".into(),
957 tolerance: 0.0,
958 };
959 assert_eq!(eval!(&p, ("a", a) => ("b", same)), Some(1.0));
960 assert_eq!(eval!(&p, ("a", a) => ("b", other)), None);
961 }
962
963 #[test]
964 fn numeric_within_non_finite_is_none() {
965 let a: HashMap<_, _> = [("year".to_string(), Value::Float(f64::NAN))].into();
966 let b: HashMap<_, _> = [("year".to_string(), Value::Float(1.0))].into();
967 let inf: HashMap<_, _> = [("year".to_string(), Value::Float(f64::INFINITY))].into();
968 let p = Predicate::NumericWithin {
969 field: "year".into(),
970 tolerance: 2.0,
971 };
972 assert_eq!(eval!(&p, ("a", a) => ("b", b)), None);
973 assert_eq!(eval!(&p, ("a", inf) => ("b", b)), None);
974 }
975
976 fn geo_pair(
977 src: (f64, f64),
978 dst: (f64, f64),
979 ) -> (HashMap<String, Value>, HashMap<String, Value>) {
980 let mk = |lat: f64, lon: f64| {
981 let mut m = HashMap::new();
982 m.insert(
983 "loc".to_string(),
984 Value::List(vec![Value::Float(lat), Value::Float(lon)]),
985 );
986 m
987 };
988 (mk(src.0, src.1), mk(dst.0, dst.1))
989 }
990
991 #[test]
992 fn geo_radius_paris_london() {
993 let (paris, london) = geo_pair((48.8566, 2.3522), (51.5074, -0.1278));
995 let inside = Predicate::GeoRadius {
996 field: "loc".into(),
997 km: 400.0,
998 };
999 let score = eval!(&inside, ("p", paris) => ("l", london)).unwrap();
1000 assert!((score - 0.14125).abs() < 0.001);
1002 let outside = Predicate::GeoRadius {
1003 field: "loc".into(),
1004 km: 300.0,
1005 };
1006 assert_eq!(eval!(&outside, ("p", paris) => ("l", london)), None);
1007 }
1008
1009 #[test]
1010 fn geo_radius_identical_coordinates_score_one() {
1011 let (a, b) = geo_pair((48.8566, 2.3522), (48.8566, 2.3522));
1012 let p = Predicate::GeoRadius {
1013 field: "loc".into(),
1014 km: 400.0,
1015 };
1016 assert_eq!(eval!(&p, ("a", a) => ("b", b)), Some(1.0));
1017 }
1018
1019 #[test]
1020 fn geo_radius_malformed_is_none() {
1021 let paris: HashMap<_, _> = [(
1022 "loc".to_string(),
1023 Value::List(vec![Value::Float(48.8566), Value::Float(2.3522)]),
1024 )]
1025 .into();
1026 let one: HashMap<_, _> =
1027 [("loc".to_string(), Value::List(vec![Value::Float(48.8566)]))].into();
1028 let three: HashMap<_, _> = [(
1029 "loc".to_string(),
1030 Value::List(vec![
1031 Value::Float(48.8566),
1032 Value::Float(2.3522),
1033 Value::Float(0.0),
1034 ]),
1035 )]
1036 .into();
1037 let string_el: HashMap<_, _> = [(
1038 "loc".to_string(),
1039 Value::List(vec![Value::Str("48.8566".into()), Value::Float(2.3522)]),
1040 )]
1041 .into();
1042 let lat91: HashMap<_, _> = [(
1043 "loc".to_string(),
1044 Value::List(vec![Value::Float(91.0), Value::Float(0.0)]),
1045 )]
1046 .into();
1047 let p = Predicate::GeoRadius {
1048 field: "loc".into(),
1049 km: 400.0,
1050 };
1051 assert_eq!(eval!(&p, ("a", paris) => ("b", one)), None);
1052 assert_eq!(eval!(&p, ("a", paris) => ("b", three)), None);
1053 assert_eq!(eval!(&p, ("a", paris) => ("b", string_el)), None);
1054 assert_eq!(eval!(&p, ("a", paris) => ("b", lat91)), None);
1055 }
1056
1057 fn vec_field(vals: &[f64]) -> HashMap<String, Value> {
1058 [(
1059 "emb".to_string(),
1060 Value::List(vals.iter().copied().map(Value::Float).collect()),
1061 )]
1062 .into()
1063 }
1064
1065 #[test]
1066 fn vector_similar_cosine_and_rejects() {
1067 let a = vec_field(&[1.0, 0.0]);
1068 let same = vec_field(&[1.0, 0.0]);
1069 let ortho = vec_field(&[0.0, 1.0]);
1070 let p = Predicate::VectorSimilar {
1071 field: "emb".into(),
1072 min: 0.5,
1073 };
1074 assert_eq!(eval!(&p, ("a", a) => ("b", same)), Some(1.0));
1075 assert_eq!(eval!(&p, ("a", a) => ("b", ortho)), None); let u = vec_field(&[1.0, 2.0]);
1078 let scaled = vec_field(&[2.0, 4.0]);
1079 let score = eval!(&p, ("a", u) => ("b", scaled)).unwrap();
1080 assert!((1.0 - score).abs() < 1e-9); let dim3 = vec_field(&[1.0, 0.0, 0.0]);
1083 assert_eq!(eval!(&p, ("a", a) => ("b", dim3)), None);
1084 let zero = vec_field(&[0.0, 0.0]);
1085 assert_eq!(eval!(&p, ("a", a) => ("b", zero)), None);
1086 }
1087
1088 #[test]
1089 fn approximate_only_valid_with_vector_similar_rooted_predicate() {
1090 let ok_vec = RuleDef {
1092 name: "av".into(),
1093 src_label: "V".into(),
1094 dst_label: "V".into(),
1095 predicate: Predicate::VectorSimilar {
1096 field: "emb".into(),
1097 min: 0.9,
1098 },
1099 edge_type: "VEC".into(),
1100 weight_prop: None,
1101 max_edges: None,
1102 approximate: true,
1103 via_label: None,
1104 via_edge: None,
1105 via_dir: None,
1106 namespace: None,
1107 };
1108 assert!(ok_vec.validate().is_ok());
1109
1110 let ok_all = RuleDef {
1112 name: "av2".into(),
1113 src_label: "V".into(),
1114 dst_label: "V".into(),
1115 predicate: Predicate::All(vec![
1116 Predicate::VectorSimilar {
1117 field: "emb".into(),
1118 min: 0.9,
1119 },
1120 Predicate::FieldEqual {
1121 field: "kind".into(),
1122 },
1123 ]),
1124 edge_type: "VEC2".into(),
1125 weight_prop: None,
1126 max_edges: None,
1127 approximate: true,
1128 via_label: None,
1129 via_edge: None,
1130 via_dir: None,
1131 namespace: None,
1132 };
1133 assert!(ok_all.validate().is_ok());
1134
1135 let bad_fe = RuleDef {
1137 name: "bfe".into(),
1138 src_label: "A".into(),
1139 dst_label: "A".into(),
1140 predicate: Predicate::FieldEqual { field: "f".into() },
1141 edge_type: "FE".into(),
1142 weight_prop: None,
1143 max_edges: None,
1144 approximate: true,
1145 via_label: None,
1146 via_edge: None,
1147 via_dir: None,
1148 namespace: None,
1149 };
1150 assert!(bad_fe.validate().is_err());
1151
1152 let bad_ov = RuleDef {
1154 name: "bov".into(),
1155 src_label: "A".into(),
1156 dst_label: "A".into(),
1157 predicate: Predicate::Overlap {
1158 field: "tags".into(),
1159 min: 0.5,
1160 },
1161 edge_type: "OV".into(),
1162 weight_prop: None,
1163 max_edges: None,
1164 approximate: true,
1165 via_label: None,
1166 via_edge: None,
1167 via_dir: None,
1168 namespace: None,
1169 };
1170 assert!(bad_ov.validate().is_err());
1171
1172 let bad_all_order = RuleDef {
1174 name: "bao".into(),
1175 src_label: "A".into(),
1176 dst_label: "A".into(),
1177 predicate: Predicate::All(vec![
1178 Predicate::FieldEqual { field: "f".into() },
1179 Predicate::VectorSimilar {
1180 field: "emb".into(),
1181 min: 0.9,
1182 },
1183 ]),
1184 edge_type: "E".into(),
1185 weight_prop: None,
1186 max_edges: None,
1187 approximate: true,
1188 via_label: None,
1189 via_edge: None,
1190 via_dir: None,
1191 namespace: None,
1192 };
1193 assert!(bad_all_order.validate().is_err());
1194 }
1195
1196 #[test]
1197 fn validate_rejects_via_with_approximate() {
1198 let bad = RuleDef {
1200 name: "vbad".into(),
1201 src_label: "A".into(),
1202 dst_label: "B".into(),
1203 predicate: Predicate::VectorSimilar {
1204 field: "emb".into(),
1205 min: 0.9,
1206 },
1207 edge_type: "VEC".into(),
1208 weight_prop: None,
1209 max_edges: None,
1210 approximate: true,
1211 via_label: Some("Mid".into()),
1212 via_edge: Some("hop".into()),
1213 via_dir: None,
1214 namespace: None,
1215 };
1216 let err = bad.validate().unwrap_err();
1217 assert_eq!(err, "via-hop rules do not support approximate: true");
1218
1219 let ok = RuleDef {
1221 approximate: false,
1222 ..bad.clone()
1223 };
1224 assert!(ok.validate().is_ok());
1225 }
1226
1227 #[test]
1228 fn all_composes_field_equal_and_numeric_within() {
1229 let a: HashMap<_, _> = [
1230 ("ind".to_string(), Value::Str("arch".into())),
1231 ("year".to_string(), Value::Int(1998)),
1232 ]
1233 .into();
1234 let b: HashMap<_, _> = [
1235 ("ind".to_string(), Value::Str("arch".into())),
1236 ("year".to_string(), Value::Float(2000.0)),
1237 ]
1238 .into();
1239 let p = Predicate::All(vec![
1240 Predicate::FieldEqual {
1241 field: "ind".into(),
1242 },
1243 Predicate::NumericWithin {
1244 field: "year".into(),
1245 tolerance: 3.0,
1246 },
1247 ]);
1248 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1249 assert!((s - 1.0 / 3.0).abs() < 1e-9); }
1251
1252 fn sample_rule(pred: Predicate) -> RuleDef {
1253 RuleDef {
1254 name: "r".into(),
1255 src_label: "A".into(),
1256 dst_label: "B".into(),
1257 predicate: pred,
1258 edge_type: "E".into(),
1259 weight_prop: None,
1260 max_edges: None,
1261 approximate: false,
1262 via_label: None,
1263 via_edge: None,
1264 via_dir: None,
1265 namespace: None,
1266 }
1267 }
1268
1269 #[test]
1275 fn any_takes_max_score_and_requires_at_least_one_branch() {
1276 let mk =
1277 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1278 let a: HashMap<_, _> = [
1283 ("ind".to_string(), Value::Str("arch".into())),
1284 ("tags".to_string(), mk(&["x", "y"])),
1285 ]
1286 .into();
1287 let b: HashMap<_, _> = [
1288 ("ind".to_string(), Value::Str("law".into())),
1289 ("tags".to_string(), mk(&["y", "z"])),
1290 ]
1291 .into();
1292 let p = Predicate::Any(vec![
1293 Predicate::FieldEqual {
1294 field: "ind".into(),
1295 },
1296 Predicate::Overlap {
1297 field: "tags".into(),
1298 min: 0.3,
1299 },
1300 ]);
1301 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1302 assert!(
1303 (s - 1.0 / 3.0).abs() < 1e-9,
1304 "score must be max(None, 1/3) = 1/3, got {s}"
1305 );
1306 }
1307
1308 #[test]
1310 fn any_score_is_max_when_both_branches_match() {
1311 let a: HashMap<_, _> = [
1316 ("ind".to_string(), Value::Str("arch".into())),
1317 ("year".to_string(), Value::Int(2000)),
1318 ]
1319 .into();
1320 let b: HashMap<_, _> = [
1321 ("ind".to_string(), Value::Str("arch".into())),
1322 ("year".to_string(), Value::Float(2001.0)),
1323 ]
1324 .into();
1325 let p = Predicate::Any(vec![
1326 Predicate::FieldEqual {
1327 field: "ind".into(),
1328 },
1329 Predicate::NumericWithin {
1330 field: "year".into(),
1331 tolerance: 3.0,
1332 },
1333 ]);
1334 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1335 assert!(
1336 (s - 1.0).abs() < 1e-9,
1337 "score must be max(1.0, 2/3) = 1.0, got {s}"
1338 );
1339 }
1340
1341 #[test]
1343 fn any_returns_none_when_all_branches_fail() {
1344 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
1345 let b: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
1346 let p = Predicate::Any(vec![
1347 Predicate::FieldEqual {
1348 field: "ind".into(),
1349 },
1350 Predicate::FieldEqual {
1351 field: "ind".into(),
1352 },
1353 ]);
1354 assert_eq!(eval!(&p, ("a", a) => ("b", b)), None);
1355 }
1356
1357 #[test]
1360 fn nested_all_of_any_uses_min_over_max() {
1361 let mk =
1362 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1363 let a: HashMap<_, _> = [
1371 ("ind".to_string(), Value::Str("arch".into())),
1372 ("tags".to_string(), mk(&["x", "y"])),
1373 ("year".to_string(), Value::Int(2000)),
1374 ]
1375 .into();
1376 let b: HashMap<_, _> = [
1377 ("ind".to_string(), Value::Str("arch".into())),
1378 ("tags".to_string(), mk(&["y", "z"])),
1379 ("year".to_string(), Value::Float(2001.0)),
1380 ]
1381 .into();
1382 let p = Predicate::All(vec![
1383 Predicate::FieldEqual {
1384 field: "ind".into(),
1385 },
1386 Predicate::Any(vec![
1387 Predicate::Overlap {
1388 field: "tags".into(),
1389 min: 0.3,
1390 },
1391 Predicate::NumericWithin {
1392 field: "year".into(),
1393 tolerance: 3.0,
1394 },
1395 ]),
1396 ]);
1397 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1398 assert!(
1399 (s - 2.0 / 3.0).abs() < 1e-9,
1400 "expected min(1.0, max(1/3, 2/3)) = 2/3, got {s}"
1401 );
1402 }
1403
1404 #[test]
1407 fn nested_any_of_all_uses_max_over_min() {
1408 let p = Predicate::Any(vec![
1410 Predicate::All(vec![
1411 Predicate::FieldEqual {
1412 field: "gen".into(),
1413 },
1414 Predicate::NumericWithin {
1415 field: "yr".into(),
1416 tolerance: 4.0,
1417 },
1418 ]),
1419 Predicate::NumericWithin {
1420 field: "yr2".into(),
1421 tolerance: 10.0,
1422 },
1423 ]);
1424
1425 let a: HashMap<_, _> = [
1432 ("gen".to_string(), Value::Str("pop".into())),
1433 ("yr".to_string(), Value::Int(2000)),
1434 ("yr2".to_string(), Value::Int(2000)),
1435 ]
1436 .into();
1437 let b: HashMap<_, _> = [
1438 ("gen".to_string(), Value::Str("pop".into())),
1439 ("yr".to_string(), Value::Float(2001.0)),
1440 ("yr2".to_string(), Value::Float(2005.0)),
1441 ]
1442 .into();
1443 let s_a = eval!(&p, ("a", a) => ("b", b)).unwrap();
1444 assert!(
1445 (s_a - 0.75).abs() < 1e-9,
1446 "Any-of-All scenario A: max(min(1.0,0.75), 0.5) must be 0.75, got {s_a}"
1447 );
1448
1449 let a2: HashMap<_, _> = [
1454 ("gen".to_string(), Value::Str("pop".into())),
1455 ("yr".to_string(), Value::Int(2000)),
1456 ("yr2".to_string(), Value::Int(2000)),
1457 ]
1458 .into();
1459 let c: HashMap<_, _> = [
1460 ("gen".to_string(), Value::Str("rock".into())),
1461 ("yr".to_string(), Value::Float(2001.0)),
1462 ("yr2".to_string(), Value::Float(2001.0)),
1463 ]
1464 .into();
1465 let s_b = eval!(&p, ("a", a2) => ("c", c)).unwrap();
1466 assert!(
1467 (s_b - 0.9).abs() < 1e-9,
1468 "Any-of-All scenario B: max(None, 0.9) must be 0.9, got {s_b}"
1469 );
1470 }
1471
1472 #[test]
1474 fn any_validation_errors() {
1475 let empty = sample_rule(Predicate::Any(vec![]));
1477 let err = empty.validate().unwrap_err();
1478 assert_eq!(err, "any() must have at least one predicate");
1479
1480 fn any_chain(depth: usize) -> Predicate {
1482 if depth == 0 {
1483 Predicate::FieldEqual { field: "f".into() }
1484 } else {
1485 Predicate::Any(vec![any_chain(depth - 1)])
1486 }
1487 }
1488
1489 assert!(
1491 sample_rule(any_chain(4)).validate().is_ok(),
1492 "depth 4 must be valid (at cap)"
1493 );
1494 let too_deep = sample_rule(any_chain(5));
1496 let err = too_deep.validate().unwrap_err();
1497 assert!(
1498 err.contains("nesting depth"),
1499 "error must mention 'nesting depth', got: {err}"
1500 );
1501
1502 let bad_inner = sample_rule(Predicate::Any(vec![Predicate::All(vec![])]));
1504 assert!(bad_inner.validate().is_err());
1505 }
1506
1507 #[test]
1509 fn any_watched_fields_collected() {
1510 let p = Predicate::Any(vec![
1511 Predicate::FieldEqual {
1512 field: "ind".into(),
1513 },
1514 Predicate::NumericWithin {
1515 field: "year".into(),
1516 tolerance: 1.0,
1517 },
1518 ]);
1519 let r = sample_rule(p);
1520 assert!(r.validate().is_ok());
1521 let fields: Vec<_> = r.watched_fields().into_iter().collect();
1522 assert_eq!(fields, vec!["ind".to_string(), "year".to_string()]);
1523 }
1524
1525 #[test]
1526 fn new_predicates_validate_and_watch_fields() {
1527 let num = sample_rule(Predicate::NumericWithin {
1528 field: "year".into(),
1529 tolerance: 2.0,
1530 });
1531 assert!(num.validate().is_ok());
1532 assert_eq!(
1533 num.watched_fields().into_iter().collect::<Vec<_>>(),
1534 vec!["year".to_string()]
1535 );
1536 let geo = sample_rule(Predicate::GeoRadius {
1537 field: "loc".into(),
1538 km: 400.0,
1539 });
1540 assert!(geo.validate().is_ok());
1541 let vecp = sample_rule(Predicate::VectorSimilar {
1542 field: "emb".into(),
1543 min: 0.9,
1544 });
1545 assert!(vecp.validate().is_ok());
1546
1547 let mut bad = num.clone();
1548 bad.predicate = Predicate::NumericWithin {
1549 field: "year".into(),
1550 tolerance: -1.0,
1551 };
1552 assert!(bad.validate().is_err());
1553 bad.predicate = Predicate::NumericWithin {
1554 field: "year".into(),
1555 tolerance: f64::NAN,
1556 };
1557 assert!(bad.validate().is_err());
1558
1559 let mut bad_geo = geo;
1560 bad_geo.predicate = Predicate::GeoRadius {
1561 field: "loc".into(),
1562 km: 0.0,
1563 };
1564 assert!(bad_geo.validate().is_err());
1565 bad_geo.predicate = Predicate::GeoRadius {
1566 field: "loc".into(),
1567 km: f64::NAN,
1568 };
1569 assert!(bad_geo.validate().is_err());
1570
1571 let mut bad_vec = vecp;
1572 bad_vec.predicate = Predicate::VectorSimilar {
1573 field: "emb".into(),
1574 min: 0.0,
1575 };
1576 assert!(bad_vec.validate().is_err());
1577 bad_vec.predicate = Predicate::VectorSimilar {
1578 field: "emb".into(),
1579 min: 1.5,
1580 };
1581 assert!(bad_vec.validate().is_err());
1582 }
1583
1584 #[test]
1585 fn default_max_edges_keymatch_is_512_else_32() {
1586 assert_eq!(DEFAULT_SCORED_TOP_K, 32);
1587 assert_eq!(DEFAULT_KEYMATCH_TOP_K, 512);
1589 assert_eq!(DEFAULT_KEYMATCH_TOP_K, MAX_KEYMATCH_LIST as u64);
1590 assert_eq!(
1591 default_max_edges(&Predicate::KeyMatch { field: "fk".into() }),
1592 DEFAULT_KEYMATCH_TOP_K
1593 );
1594 assert_eq!(
1595 default_max_edges(&Predicate::All(vec![Predicate::KeyMatch {
1596 field: "fk".into()
1597 }])),
1598 DEFAULT_KEYMATCH_TOP_K
1599 );
1600 assert_eq!(
1601 default_max_edges(&Predicate::All(vec![Predicate::All(vec![
1602 Predicate::KeyMatch { field: "fk".into() }
1603 ])])),
1604 DEFAULT_KEYMATCH_TOP_K
1605 );
1606 assert_eq!(
1607 default_max_edges(&Predicate::Overlap {
1608 field: "tags".into(),
1609 min: 0.5,
1610 }),
1611 DEFAULT_SCORED_TOP_K
1612 );
1613 assert_eq!(
1614 default_max_edges(&Predicate::Any(vec![Predicate::KeyMatch {
1615 field: "fk".into()
1616 }])),
1617 DEFAULT_SCORED_TOP_K
1618 );
1619 assert!(!is_keymatch_rooted(&Predicate::FieldEqual {
1620 field: "f".into()
1621 }));
1622 }
1623}
1624
1625#[cfg(test)]
1626mod wire_pins {
1627 use super::*;
1628
1629 fn pin(pred: Predicate) -> RuleDef {
1630 RuleDef {
1631 name: "r".into(),
1632 src_label: "A".into(),
1633 dst_label: "B".into(),
1634 predicate: pred,
1635 edge_type: "E".into(),
1636 weight_prop: None,
1637 max_edges: None,
1638 approximate: false,
1639 via_label: None,
1640 via_edge: None,
1641 via_dir: None,
1642 namespace: None,
1643 }
1644 }
1645
1646 fn pin_approx(pred: Predicate) -> RuleDef {
1647 RuleDef {
1648 name: "r".into(),
1649 src_label: "A".into(),
1650 dst_label: "B".into(),
1651 predicate: pred,
1652 edge_type: "E".into(),
1653 weight_prop: None,
1654 max_edges: None,
1655 approximate: true,
1656 via_label: None,
1657 via_edge: None,
1658 via_dir: None,
1659 namespace: None,
1660 }
1661 }
1662
1663 #[test]
1664 fn old_predicate_variants_keep_encoding() {
1665 assert_eq!(
1672 bincode::serialize(&pin(Predicate::KeyMatch { field: "fk".into() })).unwrap(),
1673 vec![
1674 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1675 66, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 102, 107, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0,
1676 0, 0, 0, 0, 0
1677 ]
1678 );
1679 assert_eq!(
1680 bincode::serialize(&pin(Predicate::FieldEqual {
1681 field: "ind".into()
1682 }))
1683 .unwrap(),
1684 vec![
1685 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1686 66, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 105, 110, 100, 1, 0, 0, 0, 0, 0, 0, 0, 69,
1687 0, 0, 0, 0, 0, 0, 0
1688 ]
1689 );
1690 assert_eq!(
1691 bincode::serialize(&pin(Predicate::Overlap {
1692 field: "tags".into(),
1693 min: 0.5,
1694 }))
1695 .unwrap(),
1696 vec![
1697 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1698 66, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 116, 97, 103, 115, 0, 0, 0, 0, 0, 0, 224,
1699 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 0
1700 ]
1701 );
1702 assert_eq!(
1703 bincode::serialize(&pin(Predicate::All(vec![
1704 Predicate::KeyMatch { field: "fk".into() },
1705 Predicate::Overlap {
1706 field: "tags".into(),
1707 min: 0.5,
1708 },
1709 ])))
1710 .unwrap(),
1711 vec![
1712 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1713 66, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 102,
1714 107, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 116, 97, 103, 115, 0, 0, 0, 0, 0, 0, 224,
1715 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 0
1716 ]
1717 );
1718 }
1719
1720 #[test]
1721 fn new_predicate_variants_have_pinned_encoding() {
1722 assert_eq!(
1724 bincode::serialize(&pin(Predicate::NumericWithin {
1725 field: "year".into(),
1726 tolerance: 2.0,
1727 }))
1728 .unwrap(),
1729 vec![
1730 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1731 66, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 121, 101, 97, 114, 0, 0, 0, 0, 0, 0, 0, 64,
1732 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 0
1733 ]
1734 );
1735 assert_eq!(
1736 bincode::serialize(&pin(Predicate::GeoRadius {
1737 field: "loc".into(),
1738 km: 400.0,
1739 }))
1740 .unwrap(),
1741 vec![
1742 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1743 66, 5, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 108, 111, 99, 0, 0, 0, 0, 0, 0, 121, 64, 1,
1744 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 0
1745 ]
1746 );
1747 assert_eq!(
1748 bincode::serialize(&pin(Predicate::VectorSimilar {
1749 field: "emb".into(),
1750 min: 0.9,
1751 }))
1752 .unwrap(),
1753 vec![
1754 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1755 66, 6, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 101, 109, 98, 205, 204, 204, 204, 204, 204,
1756 236, 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 0
1757 ]
1758 );
1759 }
1760
1761 #[test]
1762 fn any_variant_is_appended_at_discriminant_7() {
1763 let any_fe = pin(Predicate::Any(vec![Predicate::FieldEqual {
1779 field: "f".into(),
1780 }]));
1781 assert_eq!(
1782 bincode::serialize(&any_fe).unwrap(),
1783 vec![
1784 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1785 66, 7, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 102, 1,
1786 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 0
1787 ],
1788 "Any([FieldEqual{{f}}]) exact-bytes pin failed — discriminant or field layout changed"
1789 );
1790 let decoded: RuleDef = bincode::deserialize(&bincode::serialize(&any_fe).unwrap()).unwrap();
1792 assert_eq!(decoded, any_fe, "Any must round-trip via bincode");
1793 let vs = pin(Predicate::VectorSimilar {
1796 field: "emb".into(),
1797 min: 0.9,
1798 });
1799 let vs_bytes = bincode::serialize(&vs).unwrap();
1800 let vs_decoded: RuleDef = bincode::deserialize(&vs_bytes).unwrap();
1801 assert_eq!(
1802 vs_decoded.predicate,
1803 Predicate::VectorSimilar {
1804 field: "emb".into(),
1805 min: 0.9
1806 },
1807 "VectorSimilar record must still decode after via fields appended"
1808 );
1809 }
1810
1811 #[test]
1812 fn approximate_variant_has_pinned_encoding() {
1813 assert_eq!(
1817 bincode::serialize(&pin_approx(Predicate::VectorSimilar {
1818 field: "emb".into(),
1819 min: 0.9,
1820 }))
1821 .unwrap(),
1822 vec![
1823 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1824 66, 6, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 101, 109, 98, 205, 204, 204, 204, 204, 204,
1825 236, 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 1, 0, 0, 0, 0
1826 ]
1827 );
1828 let exact = bincode::serialize(&pin(Predicate::VectorSimilar {
1832 field: "emb".into(),
1833 min: 0.9,
1834 }))
1835 .unwrap();
1836 let approx = bincode::serialize(&pin_approx(Predicate::VectorSimilar {
1837 field: "emb".into(),
1838 min: 0.9,
1839 }))
1840 .unwrap();
1841 assert_eq!(exact.len(), approx.len());
1842 let n = exact.len();
1843 assert_eq!(&exact[..n - 5], &approx[..n - 5]);
1845 assert_eq!(exact[n - 5], 0u8, "exact: approximate=false");
1847 assert_eq!(approx[n - 5], 1u8, "approx: approximate=true");
1848 assert_eq!(&exact[n - 4..], &[0u8, 0, 0, 0]);
1850 assert_eq!(&approx[n - 4..], &[0u8, 0, 0, 0]);
1851 }
1852
1853 fn base_legacy() -> LegacyRuleDefNoVia {
1858 LegacyRuleDefNoVia {
1859 name: "r".into(),
1860 src_label: "A".into(),
1861 dst_label: "B".into(),
1862 predicate: Predicate::FieldEqual {
1863 field: "ind".into(),
1864 },
1865 edge_type: "E".into(),
1866 weight_prop: None,
1867 max_edges: Some(10),
1868 approximate: false,
1869 }
1870 }
1871
1872 fn base_current() -> RuleDef {
1873 RuleDef {
1874 name: "r".into(),
1875 src_label: "A".into(),
1876 dst_label: "B".into(),
1877 predicate: Predicate::FieldEqual {
1878 field: "ind".into(),
1879 },
1880 edge_type: "E".into(),
1881 weight_prop: None,
1882 max_edges: Some(10),
1883 approximate: false,
1884 via_label: None,
1885 via_edge: None,
1886 via_dir: None,
1887 namespace: None,
1888 }
1889 }
1890
1891 #[test]
1894 fn decode_rule_def_legacy_roundtrip() {
1895 let legacy_bytes = bincode::serialize(&base_legacy()).unwrap();
1896 let got = decode_rule_def(&legacy_bytes).expect("legacy decode must succeed");
1897 assert_eq!(got.name, "r");
1898 assert_eq!(got.src_label, "A");
1899 assert_eq!(got.max_edges, Some(10));
1900 assert!(!got.approximate);
1901 assert!(got.via_label.is_none(), "via_label must default to None");
1902 assert!(got.via_edge.is_none(), "via_edge must default to None");
1903 assert!(got.via_dir.is_none(), "via_dir must default to None");
1904 }
1905
1906 #[test]
1909 fn decode_rule_def_current_shape_roundtrip() {
1910 let current = RuleDef {
1911 via_label: Some("Mid".into()),
1912 via_edge: Some("hop".into()),
1913 via_dir: Some(core_storage::Direction::Out),
1914 namespace: None,
1915 ..base_current()
1916 };
1917 let bytes = bincode::serialize(¤t).unwrap();
1918 let got = decode_rule_def(&bytes).expect("current-shape decode must succeed");
1919 assert_eq!(got, current);
1920 }
1921
1922 #[test]
1924 fn decode_rule_def_garbage_returns_err() {
1925 let garbage = b"\xde\xad\xbe\xef\x00\x00\x00\x00";
1926 let err = decode_rule_def(garbage).unwrap_err();
1927 assert!(
1928 err.contains("current-shape"),
1929 "error must name current-shape attempt: {err}"
1930 );
1931 assert!(
1932 err.contains("legacy-shape"),
1933 "error must name legacy-shape attempt: {err}"
1934 );
1935 }
1936
1937 #[test]
1943 fn decode_rule_def_current_none_via_not_misidentified_as_legacy() {
1944 let current = base_current(); let legacy = base_legacy();
1946 let current_bytes = bincode::serialize(¤t).unwrap();
1947 let legacy_bytes = bincode::serialize(&legacy).unwrap();
1948
1949 assert_ne!(
1952 current_bytes, legacy_bytes,
1953 "current and legacy encodings must not be byte-identical"
1954 );
1955 assert_eq!(
1956 current_bytes.len(),
1957 legacy_bytes.len() + 4,
1958 "current is exactly 4 bytes longer (three via Option::None fields \
1959 plus namespace)"
1960 );
1961
1962 let from_current =
1964 decode_rule_def(¤t_bytes).expect("current-shape must decode via current path");
1965 let from_legacy =
1966 decode_rule_def(&legacy_bytes).expect("legacy bytes must decode via legacy path");
1967 assert_eq!(
1968 from_current, from_legacy,
1969 "both paths must produce the same RuleDef"
1970 );
1971 assert!(from_current.via_label.is_none());
1972 assert!(from_current.via_edge.is_none());
1973 assert!(from_current.via_dir.is_none());
1974 assert!(from_current.namespace.is_none());
1975 }
1976
1977 #[test]
1981 fn decode_rule_def_pre_namespace_shape_decodes_as_global() {
1982 let prev = LegacyRuleDefNoNamespace {
1983 name: "r".into(),
1984 src_label: "A".into(),
1985 dst_label: "B".into(),
1986 predicate: Predicate::FieldEqual {
1987 field: "ind".into(),
1988 },
1989 edge_type: "E".into(),
1990 weight_prop: None,
1991 max_edges: Some(10),
1992 approximate: false,
1993 via_label: Some("Mid".into()),
1994 via_edge: Some("hop".into()),
1995 via_dir: Some(core_storage::Direction::In),
1996 };
1997 let prev_bytes = bincode::serialize(&prev).unwrap();
1998 let got = decode_rule_def(&prev_bytes).expect("the v0.6.5 shape must still decode");
1999 assert_eq!(got.via_label.as_deref(), Some("Mid"));
2000 assert_eq!(got.via_dir, Some(core_storage::Direction::In));
2001 assert!(
2002 got.namespace.is_none(),
2003 "a rule written before namespaces existed is global"
2004 );
2005
2006 let current = RuleDef {
2009 namespace: None,
2010 ..base_current()
2011 };
2012 let current_bytes = bincode::serialize(¤t).unwrap();
2013 let eleven = bincode::serialize(&LegacyRuleDefNoNamespace {
2014 name: current.name.clone(),
2015 src_label: current.src_label.clone(),
2016 dst_label: current.dst_label.clone(),
2017 predicate: current.predicate.clone(),
2018 edge_type: current.edge_type.clone(),
2019 weight_prop: current.weight_prop.clone(),
2020 max_edges: current.max_edges,
2021 approximate: current.approximate,
2022 via_label: current.via_label.clone(),
2023 via_edge: current.via_edge.clone(),
2024 via_dir: current.via_dir,
2025 })
2026 .unwrap();
2027 assert_eq!(current_bytes.len(), eleven.len() + 1);
2028 assert_eq!(decode_rule_def(¤t_bytes).unwrap(), current);
2029
2030 let scoped = RuleDef {
2032 namespace: Some("tenant-a".into()),
2033 ..base_current()
2034 };
2035 let bytes = bincode::serialize(&scoped).unwrap();
2036 assert_eq!(decode_rule_def(&bytes).unwrap(), scoped);
2037 }
2038
2039 #[test]
2054 fn a_0_6_5_decoder_cannot_read_a_0_6_6_rule() {
2055 use bincode::Options as _;
2056 let opts = bincode::options()
2059 .with_fixint_encoding()
2060 .with_no_limit()
2061 .reject_trailing_bytes();
2062
2063 for namespace in [None, Some("tenant-a".to_string())] {
2064 let def = RuleDef {
2065 namespace,
2066 ..base_current()
2067 };
2068 let bytes = bincode::serialize(&def).unwrap();
2069
2070 assert!(
2071 opts.deserialize::<LegacyRuleDefNoNamespace>(&bytes)
2072 .is_err(),
2073 "0.6.5's eleven-field decoder must refuse 0.6.6 bytes"
2074 );
2075 assert!(
2076 opts.deserialize::<LegacyRuleDefNoVia>(&bytes).is_err(),
2077 "0.6.5's eight-field decoder must refuse them too"
2078 );
2079 assert_eq!(decode_rule_def(&bytes).unwrap(), def);
2081 }
2082 }
2083}