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}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub enum Predicate {
86 KeyMatch {
87 field: String,
88 },
89 FieldEqual {
90 field: String,
91 },
92 Overlap {
93 field: String,
94 min: f64,
95 },
96 All(Vec<Predicate>),
97 NumericWithin {
99 field: String,
100 tolerance: f64,
101 },
102 GeoRadius {
103 field: String,
104 km: f64,
105 },
106 VectorSimilar {
107 field: String,
108 min: f64,
109 },
110 Any(Vec<Predicate>),
114}
115
116pub struct NodeView<'a> {
117 pub key: &'a str,
118 pub props: &'a dyn Fn(&str) -> Option<Value>,
119}
120
121impl RuleDef {
122 pub fn validate(&self) -> Result<(), String> {
123 for (what, s) in [
124 ("name", &self.name),
125 ("src_label", &self.src_label),
126 ("dst_label", &self.dst_label),
127 ("edge_type", &self.edge_type),
128 ] {
129 if s.is_empty() {
130 return Err(format!("{what} must not be empty"));
131 }
132 }
133 validate_pred(&self.predicate)?;
134 let depth = predicate_nesting_depth(&self.predicate);
135 if depth > MAX_PREDICATE_NESTING_DEPTH {
136 return Err(format!(
137 "predicate nesting depth {depth} exceeds cap of \
138 {MAX_PREDICATE_NESTING_DEPTH}"
139 ));
140 }
141 if self.approximate && !predicate_is_vector_similar_rooted(&self.predicate) {
142 return Err(
143 "approximate=true requires a VectorSimilar-rooted predicate \
144 (VectorSimilar, or All whose first element is VectorSimilar)"
145 .into(),
146 );
147 }
148 if self.via_label.is_some() && self.approximate {
149 return Err("via-hop rules do not support approximate: true".into());
150 }
151 match (&self.via_label, &self.via_edge) {
153 (Some(_), None) | (None, Some(_)) => {
154 return Err("via_label and via_edge must both be set or both absent".into());
155 }
156 (Some(l), Some(e)) => {
157 if l.is_empty() {
158 return Err("via_label must not be empty".into());
159 }
160 if e.is_empty() {
161 return Err("via_edge must not be empty".into());
162 }
163 }
164 (None, None) => {}
165 }
166 Ok(())
167 }
168
169 pub fn watched_fields(&self) -> BTreeSet<String> {
170 let mut out = BTreeSet::new();
171 collect_fields(&self.predicate, &mut out);
172 out
173 }
174}
175
176pub const MAX_PREDICATE_NESTING_DEPTH: usize = 4;
183
184pub const DEFAULT_SCORED_TOP_K: u64 = 32;
186
187pub const DEFAULT_KEYMATCH_TOP_K: u64 = MAX_KEYMATCH_LIST as u64;
194
195pub const MAX_KEYMATCH_LIST: usize = 512;
204
205pub fn is_keymatch_rooted(p: &Predicate) -> bool {
208 match p {
209 Predicate::KeyMatch { .. } => true,
210 Predicate::All(parts) => !parts.is_empty() && is_keymatch_rooted(&parts[0]),
211 Predicate::Any(_) => false,
212 _ => false,
213 }
214}
215
216pub fn predicate_contains_keymatch(p: &Predicate) -> bool {
227 match p {
228 Predicate::KeyMatch { .. } => true,
229 Predicate::All(parts) | Predicate::Any(parts) => {
230 parts.iter().any(predicate_contains_keymatch)
231 }
232 _ => false,
233 }
234}
235
236pub fn default_max_edges(predicate: &Predicate) -> u64 {
238 if is_keymatch_rooted(predicate) {
239 DEFAULT_KEYMATCH_TOP_K
240 } else {
241 DEFAULT_SCORED_TOP_K
242 }
243}
244
245pub fn predicate_is_vector_similar_rooted(p: &Predicate) -> bool {
249 match p {
250 Predicate::VectorSimilar { .. } => true,
251 Predicate::All(parts) => {
252 !parts.is_empty() && matches!(parts[0], Predicate::VectorSimilar { .. })
253 }
254 Predicate::Any(_) => false,
255 _ => false,
256 }
257}
258
259fn predicate_nesting_depth(p: &Predicate) -> usize {
265 match p {
266 Predicate::All(parts) | Predicate::Any(parts) => {
267 1 + parts.iter().map(predicate_nesting_depth).max().unwrap_or(0)
268 }
269 _ => 0,
270 }
271}
272
273fn validate_pred(p: &Predicate) -> Result<(), String> {
274 match p {
275 Predicate::KeyMatch { field } | Predicate::FieldEqual { field } => {
276 if field.is_empty() {
277 Err("field must not be empty".into())
278 } else {
279 Ok(())
280 }
281 }
282 Predicate::Overlap { field, min } => {
283 if field.is_empty() {
284 Err("field must not be empty".into())
285 } else if !(*min > 0.0 && *min <= 1.0) {
286 Err(format!("overlap min must be in (0,1], got {min}"))
287 } else {
288 Ok(())
289 }
290 }
291 Predicate::NumericWithin { field, tolerance } => {
292 if field.is_empty() {
293 Err("field must not be empty".into())
294 } else if !(tolerance.is_finite() && *tolerance >= 0.0) {
295 Err(format!(
296 "numeric_within tolerance must be finite and >= 0, got {tolerance}"
297 ))
298 } else {
299 Ok(())
300 }
301 }
302 Predicate::GeoRadius { field, km } => {
303 if field.is_empty() {
304 Err("field must not be empty".into())
305 } else if !(km.is_finite() && *km > 0.0) {
306 Err(format!("geo_radius km must be finite and > 0, got {km}"))
307 } else {
308 Ok(())
309 }
310 }
311 Predicate::VectorSimilar { field, min } => {
312 if field.is_empty() {
313 Err("field must not be empty".into())
314 } else if !(*min > 0.0 && *min <= 1.0) {
315 Err(format!("vector_similar min must be in (0,1], got {min}"))
316 } else {
317 Ok(())
318 }
319 }
320 Predicate::All(parts) => {
321 if parts.is_empty() {
322 return Err("all() must have at least one predicate".into());
323 }
324 parts.iter().try_for_each(validate_pred)
325 }
326 Predicate::Any(parts) => {
327 if parts.is_empty() {
328 return Err("any() must have at least one predicate".into());
329 }
330 parts.iter().try_for_each(validate_pred)
331 }
332 }
333}
334
335fn collect_fields(p: &Predicate, out: &mut BTreeSet<String>) {
336 match p {
337 Predicate::KeyMatch { field }
338 | Predicate::FieldEqual { field }
339 | Predicate::Overlap { field, .. }
340 | Predicate::NumericWithin { field, .. }
341 | Predicate::GeoRadius { field, .. }
342 | Predicate::VectorSimilar { field, .. } => {
343 out.insert(field.clone());
344 }
345 Predicate::All(parts) | Predicate::Any(parts) => {
346 parts.iter().for_each(|q| collect_fields(q, out))
347 }
348 }
349}
350
351pub fn evaluate(pred: &Predicate, src: &NodeView, dst: &NodeView) -> Option<f64> {
352 match pred {
353 Predicate::KeyMatch { field } => match (src.props)(field)? {
354 Value::Str(s) if s == dst.key => Some(1.0),
355 Value::List(items) => items
360 .iter()
361 .take(MAX_KEYMATCH_LIST)
362 .any(|v| matches!(v, Value::Str(s) if s == dst.key))
363 .then_some(1.0),
364 _ => None,
365 },
366 Predicate::FieldEqual { field } => {
367 let a = ValueKey::from_value(&(src.props)(field)?)?;
368 let b = ValueKey::from_value(&(dst.props)(field)?)?;
369 (a == b).then_some(1.0)
370 }
371 Predicate::Overlap { field, min } => {
372 let a = list_tokens(&(src.props)(field)?)?;
373 let b = list_tokens(&(dst.props)(field)?)?;
374 let inter = a.intersection(&b).count();
375 let union = a.union(&b).count();
376 if union == 0 || inter == 0 {
377 return None;
378 }
379 let j = inter as f64 / union as f64;
380 (j >= *min).then_some(j)
381 }
382 Predicate::All(parts) => {
383 if parts.is_empty() {
385 return None;
386 }
387 let mut score = f64::INFINITY;
388 for part in parts {
389 score = score.min(evaluate(part, src, dst)?);
390 }
391 Some(score)
392 }
393 Predicate::Any(parts) => {
394 let mut best: Option<f64> = None;
398 for part in parts {
399 if let Some(s) = evaluate(part, src, dst) {
400 best = Some(match best {
401 None => s,
402 Some(prev) => prev.max(s),
403 });
404 }
405 }
406 best
407 }
408 Predicate::NumericWithin { field, tolerance } => {
409 if !tolerance.is_finite() || *tolerance < 0.0 {
413 return None;
414 }
415 let a = as_finite_f64(&(src.props)(field)?)?;
416 let b = as_finite_f64(&(dst.props)(field)?)?;
417 let delta = (a - b).abs();
418 if *tolerance == 0.0 {
419 return (delta == 0.0).then_some(1.0);
420 }
421 (delta <= *tolerance).then_some(1.0 - delta / *tolerance)
422 }
423 Predicate::GeoRadius { field, km } => {
424 if !km.is_finite() || *km <= 0.0 {
425 return None;
426 }
427 let (alat, alon) = as_latlon(&(src.props)(field)?)?;
428 let (blat, blon) = as_latlon(&(dst.props)(field)?)?;
429 let d = haversine_km(alat, alon, blat, blon);
430 if !d.is_finite() {
431 return None;
432 }
433 (d <= *km).then_some(1.0 - d / *km)
434 }
435 Predicate::VectorSimilar { field, min } => {
436 let a = as_numeric_list(&(src.props)(field)?)?;
437 let b = as_numeric_list(&(dst.props)(field)?)?;
438 if a.len() != b.len() {
439 return None;
440 }
441 let cos = cosine(&a, &b)?.min(1.0);
442 (cos >= *min).then_some(cos)
443 }
444 }
445}
446
447fn as_finite_f64(v: &Value) -> Option<f64> {
448 match v {
449 Value::Int(i) => Some(*i as f64),
450 Value::Float(f) if f.is_finite() => Some(*f),
451 _ => None,
452 }
453}
454
455fn as_latlon(v: &Value) -> Option<(f64, f64)> {
456 let Value::List(items) = v else {
457 return None;
458 };
459 if items.len() != 2 {
460 return None;
461 }
462 let lat = as_finite_f64(&items[0])?;
463 let lon = as_finite_f64(&items[1])?;
464 if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon) {
465 Some((lat, lon))
466 } else {
467 None
468 }
469}
470
471fn as_numeric_list(v: &Value) -> Option<Vec<f64>> {
472 let Value::List(items) = v else {
473 return None;
474 };
475 if items.is_empty() {
476 return None;
477 }
478 items.iter().map(as_finite_f64).collect()
479}
480
481const EARTH_RADIUS_KM: f64 = 6371.0088;
483
484fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
485 let phi1 = lat1.to_radians();
486 let phi2 = lat2.to_radians();
487 let dphi = (lat2 - lat1).to_radians();
488 let dlam = (lon2 - lon1).to_radians();
489 let a = ((dphi / 2.0).sin().powi(2) + phi1.cos() * phi2.cos() * (dlam / 2.0).sin().powi(2))
490 .clamp(0.0, 1.0);
491 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
492 EARTH_RADIUS_KM * c
493}
494
495fn cosine(a: &[f64], b: &[f64]) -> Option<f64> {
496 let mut dot = 0.0;
497 let mut na2 = 0.0;
498 let mut nb2 = 0.0;
499 for (x, y) in a.iter().zip(b.iter()) {
500 dot += *x * *y;
501 na2 += *x * *x;
502 nb2 += *y * *y;
503 }
504 let na = na2.sqrt();
505 let nb = nb2.sqrt();
506 if !(na > 0.0 && nb > 0.0) {
507 return None;
508 }
509 let cos = dot / (na * nb);
510 cos.is_finite().then_some(cos)
511}
512
513pub fn cosine_early_exit(
549 a: &[f64],
550 b: &[f64],
551 ckpts_a: &[f64; 8],
552 ckpts_b: &[f64; 8],
553 norm_a: f64,
554 norm_b: f64,
555 min: f64,
556) -> Option<f64> {
557 let dim = a.len();
558 if dim == 0 || dim != b.len() {
559 return None; }
561 let denom = norm_a * norm_b;
562 if !denom.is_finite() || denom == 0.0 {
563 return None;
564 }
565
566 let eps = dim as f64 * f64::EPSILON * 4.0;
572 let mut dot = 0.0f64;
573
574 for ci in 0..8usize {
575 let chunk_start = ci * dim / 8;
576 let chunk_end = if ci < 7 { (ci + 1) * dim / 8 } else { dim };
577 for k in chunk_start..chunk_end {
578 dot += a[k] * b[k];
579 }
580 if ci < 7 {
584 let bound = ckpts_a[ci + 1] * ckpts_b[ci + 1];
585 let cos_max = (dot + bound) / denom;
586 if cos_max.is_finite() && cos_max < min - eps {
587 return None;
588 }
589 }
590 }
591
592 let cos = (dot / denom).min(1.0);
594 if cos.is_finite() && cos >= min {
595 Some(cos)
596 } else {
597 None
598 }
599}
600
601#[derive(serde::Serialize, serde::Deserialize)]
615struct LegacyRuleDefNoVia {
616 name: String,
617 src_label: String,
618 dst_label: String,
619 predicate: Predicate,
620 edge_type: String,
621 weight_prop: Option<String>,
622 max_edges: Option<u64>,
623 approximate: bool,
624}
625
626pub fn decode_rule_def(bytes: &[u8]) -> Result<RuleDef, String> {
643 use bincode::Options as _;
644 let opts = bincode::options()
650 .with_fixint_encoding()
651 .with_no_limit()
652 .reject_trailing_bytes();
653
654 match opts.deserialize::<RuleDef>(bytes) {
656 Ok(def) => Ok(def),
657 Err(current_err) => {
658 match opts.deserialize::<LegacyRuleDefNoVia>(bytes) {
660 Ok(legacy) => Ok(RuleDef {
661 name: legacy.name,
662 src_label: legacy.src_label,
663 dst_label: legacy.dst_label,
664 predicate: legacy.predicate,
665 edge_type: legacy.edge_type,
666 weight_prop: legacy.weight_prop,
667 max_edges: legacy.max_edges,
668 approximate: legacy.approximate,
669 via_label: None,
670 via_edge: None,
671 via_dir: None,
672 }),
673 Err(legacy_err) => Err(format!(
674 "corrupt rule_def — current-shape: {current_err}; \
675 legacy-shape (pre-0.1.2 no-via): {legacy_err}"
676 )),
677 }
678 }
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use core_storage::Value;
686 use std::collections::HashMap;
687
688 macro_rules! eval {
694 ($p:expr, ($sk:expr, $sm:ident) => ($dk:expr, $dm:ident)) => {{
695 let sp = |f: &str| $sm.get(f).cloned();
696 let dp = |f: &str| $dm.get(f).cloned();
697 evaluate(
698 $p,
699 &NodeView {
700 key: $sk,
701 props: &sp,
702 },
703 &NodeView {
704 key: $dk,
705 props: &dp,
706 },
707 )
708 }};
709 }
710
711 #[test]
712 fn key_match_links_fk_to_key() {
713 let s: HashMap<_, _> = [("cid".to_string(), Value::Str("c1".into()))].into();
714 let d: HashMap<String, Value> = HashMap::new();
715 let p = Predicate::KeyMatch {
716 field: "cid".into(),
717 };
718 assert_eq!(eval!(&p, ("t1", s) => ("c1", d)), Some(1.0));
719 assert_eq!(eval!(&p, ("t1", s) => ("c2", d)), None);
720 assert_eq!(eval!(&p, ("t1", d) => ("c1", d)), None); }
722
723 #[test]
724 fn field_equal_needs_both_scalars_equal() {
725 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
726 let b = a.clone();
727 let c: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
728 let p = Predicate::FieldEqual {
729 field: "ind".into(),
730 };
731 assert_eq!(eval!(&p, ("a", a) => ("b", b)), Some(1.0));
732 assert_eq!(eval!(&p, ("a", a) => ("c", c)), None);
733 }
734
735 #[test]
736 fn overlap_is_jaccard_with_threshold() {
737 let mk =
738 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
739 let a: HashMap<_, _> = [("tags".to_string(), mk(&["x", "y"]))].into();
740 let b: HashMap<_, _> = [("tags".to_string(), mk(&["y", "z"]))].into();
741 let p = Predicate::Overlap {
742 field: "tags".into(),
743 min: 0.3,
744 };
745 let score = eval!(&p, ("a", a) => ("b", b)).unwrap();
747 assert!((score - 1.0 / 3.0).abs() < 1e-9);
748 let strict = Predicate::Overlap {
749 field: "tags".into(),
750 min: 0.5,
751 };
752 assert_eq!(eval!(&strict, ("a", a) => ("b", b)), None);
753 let e: HashMap<_, _> = [("tags".to_string(), mk(&[]))].into();
755 assert_eq!(eval!(&p, ("a", e) => ("b", b)), None);
756 }
757
758 #[test]
759 fn all_takes_min_score_and_requires_every_part() {
760 let mk =
761 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
762 let a: HashMap<_, _> = [
763 ("ind".to_string(), Value::Str("arch".into())),
764 ("tags".to_string(), mk(&["x", "y"])),
765 ]
766 .into();
767 let b: HashMap<_, _> = [
768 ("ind".to_string(), Value::Str("arch".into())),
769 ("tags".to_string(), mk(&["y"])),
770 ]
771 .into();
772 let p = Predicate::All(vec![
773 Predicate::FieldEqual {
774 field: "ind".into(),
775 },
776 Predicate::Overlap {
777 field: "tags".into(),
778 min: 0.4,
779 },
780 ]);
781 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
782 assert!((s - 0.5).abs() < 1e-9); }
784
785 #[test]
786 fn validation_rejects_bad_rules_and_collects_watched_fields() {
787 let ok = RuleDef {
788 name: "r".into(),
789 src_label: "A".into(),
790 dst_label: "B".into(),
791 predicate: Predicate::All(vec![
792 Predicate::KeyMatch { field: "fk".into() },
793 Predicate::Overlap {
794 field: "tags".into(),
795 min: 0.5,
796 },
797 ]),
798 edge_type: "E".into(),
799 weight_prop: Some("score".into()),
800 max_edges: None,
801 approximate: false,
802 via_label: None,
803 via_edge: None,
804 via_dir: None,
805 };
806 assert!(ok.validate().is_ok());
807 assert_eq!(
808 ok.watched_fields().into_iter().collect::<Vec<_>>(),
809 vec!["fk".to_string(), "tags".to_string()]
810 );
811 let mut bad = ok.clone();
812 bad.predicate = Predicate::Overlap {
813 field: "t".into(),
814 min: 0.0,
815 };
816 assert!(bad.validate().is_err()); let mut bad2 = ok.clone();
818 bad2.edge_type = String::new();
819 assert!(bad2.validate().is_err());
820 let mut bad3 = ok;
821 bad3.predicate = Predicate::All(vec![]);
822 assert!(bad3.validate().is_err());
823 }
824
825 #[test]
826 fn evaluate_empty_all_returns_none() {
827 let empty: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
828 let sp = |f: &str| empty.get(f).cloned();
829 let dp = |f: &str| empty.get(f).cloned();
830 let src = NodeView {
831 key: "a",
832 props: &sp,
833 };
834 let dst = NodeView {
835 key: "b",
836 props: &dp,
837 };
838 assert_eq!(evaluate(&Predicate::All(vec![]), &src, &dst), None);
839 }
840
841 #[test]
842 fn numeric_within_int_float_cross_type() {
843 let a: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
844 let b: HashMap<_, _> = [("year".to_string(), Value::Float(2000.0))].into();
845 let tight = Predicate::NumericWithin {
846 field: "year".into(),
847 tolerance: 2.0,
848 };
849 assert_eq!(eval!(&tight, ("a", a) => ("b", b)), Some(0.0));
851 let loose = Predicate::NumericWithin {
852 field: "year".into(),
853 tolerance: 3.0,
854 };
855 let score = eval!(&loose, ("a", a) => ("b", b)).unwrap();
856 assert!((score - 1.0 / 3.0).abs() < 1e-9);
857 }
858
859 #[test]
860 fn numeric_within_missing_or_non_numeric_is_none() {
861 let num: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
862 let missing: HashMap<String, Value> = HashMap::new();
863 let text: HashMap<_, _> = [("year".to_string(), Value::Str("1998".into()))].into();
864 let p = Predicate::NumericWithin {
865 field: "year".into(),
866 tolerance: 2.0,
867 };
868 assert_eq!(eval!(&p, ("a", num) => ("b", missing)), None);
869 assert_eq!(eval!(&p, ("a", missing) => ("b", num)), None);
870 assert_eq!(eval!(&p, ("a", num) => ("b", text)), None);
871 }
872
873 #[test]
874 fn numeric_within_tol_zero_requires_exact() {
875 let a: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
876 let same: HashMap<_, _> = [("year".to_string(), Value::Float(1998.0))].into();
877 let other: HashMap<_, _> = [("year".to_string(), Value::Int(1999))].into();
878 let p = Predicate::NumericWithin {
879 field: "year".into(),
880 tolerance: 0.0,
881 };
882 assert_eq!(eval!(&p, ("a", a) => ("b", same)), Some(1.0));
883 assert_eq!(eval!(&p, ("a", a) => ("b", other)), None);
884 }
885
886 #[test]
887 fn numeric_within_non_finite_is_none() {
888 let a: HashMap<_, _> = [("year".to_string(), Value::Float(f64::NAN))].into();
889 let b: HashMap<_, _> = [("year".to_string(), Value::Float(1.0))].into();
890 let inf: HashMap<_, _> = [("year".to_string(), Value::Float(f64::INFINITY))].into();
891 let p = Predicate::NumericWithin {
892 field: "year".into(),
893 tolerance: 2.0,
894 };
895 assert_eq!(eval!(&p, ("a", a) => ("b", b)), None);
896 assert_eq!(eval!(&p, ("a", inf) => ("b", b)), None);
897 }
898
899 fn geo_pair(
900 src: (f64, f64),
901 dst: (f64, f64),
902 ) -> (HashMap<String, Value>, HashMap<String, Value>) {
903 let mk = |lat: f64, lon: f64| {
904 let mut m = HashMap::new();
905 m.insert(
906 "loc".to_string(),
907 Value::List(vec![Value::Float(lat), Value::Float(lon)]),
908 );
909 m
910 };
911 (mk(src.0, src.1), mk(dst.0, dst.1))
912 }
913
914 #[test]
915 fn geo_radius_paris_london() {
916 let (paris, london) = geo_pair((48.8566, 2.3522), (51.5074, -0.1278));
918 let inside = Predicate::GeoRadius {
919 field: "loc".into(),
920 km: 400.0,
921 };
922 let score = eval!(&inside, ("p", paris) => ("l", london)).unwrap();
923 assert!((score - 0.14125).abs() < 0.001);
925 let outside = Predicate::GeoRadius {
926 field: "loc".into(),
927 km: 300.0,
928 };
929 assert_eq!(eval!(&outside, ("p", paris) => ("l", london)), None);
930 }
931
932 #[test]
933 fn geo_radius_identical_coordinates_score_one() {
934 let (a, b) = geo_pair((48.8566, 2.3522), (48.8566, 2.3522));
935 let p = Predicate::GeoRadius {
936 field: "loc".into(),
937 km: 400.0,
938 };
939 assert_eq!(eval!(&p, ("a", a) => ("b", b)), Some(1.0));
940 }
941
942 #[test]
943 fn geo_radius_malformed_is_none() {
944 let paris: HashMap<_, _> = [(
945 "loc".to_string(),
946 Value::List(vec![Value::Float(48.8566), Value::Float(2.3522)]),
947 )]
948 .into();
949 let one: HashMap<_, _> =
950 [("loc".to_string(), Value::List(vec![Value::Float(48.8566)]))].into();
951 let three: HashMap<_, _> = [(
952 "loc".to_string(),
953 Value::List(vec![
954 Value::Float(48.8566),
955 Value::Float(2.3522),
956 Value::Float(0.0),
957 ]),
958 )]
959 .into();
960 let string_el: HashMap<_, _> = [(
961 "loc".to_string(),
962 Value::List(vec![Value::Str("48.8566".into()), Value::Float(2.3522)]),
963 )]
964 .into();
965 let lat91: HashMap<_, _> = [(
966 "loc".to_string(),
967 Value::List(vec![Value::Float(91.0), Value::Float(0.0)]),
968 )]
969 .into();
970 let p = Predicate::GeoRadius {
971 field: "loc".into(),
972 km: 400.0,
973 };
974 assert_eq!(eval!(&p, ("a", paris) => ("b", one)), None);
975 assert_eq!(eval!(&p, ("a", paris) => ("b", three)), None);
976 assert_eq!(eval!(&p, ("a", paris) => ("b", string_el)), None);
977 assert_eq!(eval!(&p, ("a", paris) => ("b", lat91)), None);
978 }
979
980 fn vec_field(vals: &[f64]) -> HashMap<String, Value> {
981 [(
982 "emb".to_string(),
983 Value::List(vals.iter().copied().map(Value::Float).collect()),
984 )]
985 .into()
986 }
987
988 #[test]
989 fn vector_similar_cosine_and_rejects() {
990 let a = vec_field(&[1.0, 0.0]);
991 let same = vec_field(&[1.0, 0.0]);
992 let ortho = vec_field(&[0.0, 1.0]);
993 let p = Predicate::VectorSimilar {
994 field: "emb".into(),
995 min: 0.5,
996 };
997 assert_eq!(eval!(&p, ("a", a) => ("b", same)), Some(1.0));
998 assert_eq!(eval!(&p, ("a", a) => ("b", ortho)), None); let u = vec_field(&[1.0, 2.0]);
1001 let scaled = vec_field(&[2.0, 4.0]);
1002 let score = eval!(&p, ("a", u) => ("b", scaled)).unwrap();
1003 assert!((1.0 - score).abs() < 1e-9); let dim3 = vec_field(&[1.0, 0.0, 0.0]);
1006 assert_eq!(eval!(&p, ("a", a) => ("b", dim3)), None);
1007 let zero = vec_field(&[0.0, 0.0]);
1008 assert_eq!(eval!(&p, ("a", a) => ("b", zero)), None);
1009 }
1010
1011 #[test]
1012 fn approximate_only_valid_with_vector_similar_rooted_predicate() {
1013 let ok_vec = RuleDef {
1015 name: "av".into(),
1016 src_label: "V".into(),
1017 dst_label: "V".into(),
1018 predicate: Predicate::VectorSimilar {
1019 field: "emb".into(),
1020 min: 0.9,
1021 },
1022 edge_type: "VEC".into(),
1023 weight_prop: None,
1024 max_edges: None,
1025 approximate: true,
1026 via_label: None,
1027 via_edge: None,
1028 via_dir: None,
1029 };
1030 assert!(ok_vec.validate().is_ok());
1031
1032 let ok_all = RuleDef {
1034 name: "av2".into(),
1035 src_label: "V".into(),
1036 dst_label: "V".into(),
1037 predicate: Predicate::All(vec![
1038 Predicate::VectorSimilar {
1039 field: "emb".into(),
1040 min: 0.9,
1041 },
1042 Predicate::FieldEqual {
1043 field: "kind".into(),
1044 },
1045 ]),
1046 edge_type: "VEC2".into(),
1047 weight_prop: None,
1048 max_edges: None,
1049 approximate: true,
1050 via_label: None,
1051 via_edge: None,
1052 via_dir: None,
1053 };
1054 assert!(ok_all.validate().is_ok());
1055
1056 let bad_fe = RuleDef {
1058 name: "bfe".into(),
1059 src_label: "A".into(),
1060 dst_label: "A".into(),
1061 predicate: Predicate::FieldEqual { field: "f".into() },
1062 edge_type: "FE".into(),
1063 weight_prop: None,
1064 max_edges: None,
1065 approximate: true,
1066 via_label: None,
1067 via_edge: None,
1068 via_dir: None,
1069 };
1070 assert!(bad_fe.validate().is_err());
1071
1072 let bad_ov = RuleDef {
1074 name: "bov".into(),
1075 src_label: "A".into(),
1076 dst_label: "A".into(),
1077 predicate: Predicate::Overlap {
1078 field: "tags".into(),
1079 min: 0.5,
1080 },
1081 edge_type: "OV".into(),
1082 weight_prop: None,
1083 max_edges: None,
1084 approximate: true,
1085 via_label: None,
1086 via_edge: None,
1087 via_dir: None,
1088 };
1089 assert!(bad_ov.validate().is_err());
1090
1091 let bad_all_order = RuleDef {
1093 name: "bao".into(),
1094 src_label: "A".into(),
1095 dst_label: "A".into(),
1096 predicate: Predicate::All(vec![
1097 Predicate::FieldEqual { field: "f".into() },
1098 Predicate::VectorSimilar {
1099 field: "emb".into(),
1100 min: 0.9,
1101 },
1102 ]),
1103 edge_type: "E".into(),
1104 weight_prop: None,
1105 max_edges: None,
1106 approximate: true,
1107 via_label: None,
1108 via_edge: None,
1109 via_dir: None,
1110 };
1111 assert!(bad_all_order.validate().is_err());
1112 }
1113
1114 #[test]
1115 fn validate_rejects_via_with_approximate() {
1116 let bad = RuleDef {
1118 name: "vbad".into(),
1119 src_label: "A".into(),
1120 dst_label: "B".into(),
1121 predicate: Predicate::VectorSimilar {
1122 field: "emb".into(),
1123 min: 0.9,
1124 },
1125 edge_type: "VEC".into(),
1126 weight_prop: None,
1127 max_edges: None,
1128 approximate: true,
1129 via_label: Some("Mid".into()),
1130 via_edge: Some("hop".into()),
1131 via_dir: None,
1132 };
1133 let err = bad.validate().unwrap_err();
1134 assert_eq!(err, "via-hop rules do not support approximate: true");
1135
1136 let ok = RuleDef {
1138 approximate: false,
1139 ..bad.clone()
1140 };
1141 assert!(ok.validate().is_ok());
1142 }
1143
1144 #[test]
1145 fn all_composes_field_equal_and_numeric_within() {
1146 let a: HashMap<_, _> = [
1147 ("ind".to_string(), Value::Str("arch".into())),
1148 ("year".to_string(), Value::Int(1998)),
1149 ]
1150 .into();
1151 let b: HashMap<_, _> = [
1152 ("ind".to_string(), Value::Str("arch".into())),
1153 ("year".to_string(), Value::Float(2000.0)),
1154 ]
1155 .into();
1156 let p = Predicate::All(vec![
1157 Predicate::FieldEqual {
1158 field: "ind".into(),
1159 },
1160 Predicate::NumericWithin {
1161 field: "year".into(),
1162 tolerance: 3.0,
1163 },
1164 ]);
1165 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1166 assert!((s - 1.0 / 3.0).abs() < 1e-9); }
1168
1169 fn sample_rule(pred: Predicate) -> RuleDef {
1170 RuleDef {
1171 name: "r".into(),
1172 src_label: "A".into(),
1173 dst_label: "B".into(),
1174 predicate: pred,
1175 edge_type: "E".into(),
1176 weight_prop: None,
1177 max_edges: None,
1178 approximate: false,
1179 via_label: None,
1180 via_edge: None,
1181 via_dir: None,
1182 }
1183 }
1184
1185 #[test]
1191 fn any_takes_max_score_and_requires_at_least_one_branch() {
1192 let mk =
1193 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1194 let a: HashMap<_, _> = [
1199 ("ind".to_string(), Value::Str("arch".into())),
1200 ("tags".to_string(), mk(&["x", "y"])),
1201 ]
1202 .into();
1203 let b: HashMap<_, _> = [
1204 ("ind".to_string(), Value::Str("law".into())),
1205 ("tags".to_string(), mk(&["y", "z"])),
1206 ]
1207 .into();
1208 let p = Predicate::Any(vec![
1209 Predicate::FieldEqual {
1210 field: "ind".into(),
1211 },
1212 Predicate::Overlap {
1213 field: "tags".into(),
1214 min: 0.3,
1215 },
1216 ]);
1217 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1218 assert!(
1219 (s - 1.0 / 3.0).abs() < 1e-9,
1220 "score must be max(None, 1/3) = 1/3, got {s}"
1221 );
1222 }
1223
1224 #[test]
1226 fn any_score_is_max_when_both_branches_match() {
1227 let a: HashMap<_, _> = [
1232 ("ind".to_string(), Value::Str("arch".into())),
1233 ("year".to_string(), Value::Int(2000)),
1234 ]
1235 .into();
1236 let b: HashMap<_, _> = [
1237 ("ind".to_string(), Value::Str("arch".into())),
1238 ("year".to_string(), Value::Float(2001.0)),
1239 ]
1240 .into();
1241 let p = Predicate::Any(vec![
1242 Predicate::FieldEqual {
1243 field: "ind".into(),
1244 },
1245 Predicate::NumericWithin {
1246 field: "year".into(),
1247 tolerance: 3.0,
1248 },
1249 ]);
1250 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1251 assert!(
1252 (s - 1.0).abs() < 1e-9,
1253 "score must be max(1.0, 2/3) = 1.0, got {s}"
1254 );
1255 }
1256
1257 #[test]
1259 fn any_returns_none_when_all_branches_fail() {
1260 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
1261 let b: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
1262 let p = Predicate::Any(vec![
1263 Predicate::FieldEqual {
1264 field: "ind".into(),
1265 },
1266 Predicate::FieldEqual {
1267 field: "ind".into(),
1268 },
1269 ]);
1270 assert_eq!(eval!(&p, ("a", a) => ("b", b)), None);
1271 }
1272
1273 #[test]
1276 fn nested_all_of_any_uses_min_over_max() {
1277 let mk =
1278 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1279 let a: HashMap<_, _> = [
1287 ("ind".to_string(), Value::Str("arch".into())),
1288 ("tags".to_string(), mk(&["x", "y"])),
1289 ("year".to_string(), Value::Int(2000)),
1290 ]
1291 .into();
1292 let b: HashMap<_, _> = [
1293 ("ind".to_string(), Value::Str("arch".into())),
1294 ("tags".to_string(), mk(&["y", "z"])),
1295 ("year".to_string(), Value::Float(2001.0)),
1296 ]
1297 .into();
1298 let p = Predicate::All(vec![
1299 Predicate::FieldEqual {
1300 field: "ind".into(),
1301 },
1302 Predicate::Any(vec![
1303 Predicate::Overlap {
1304 field: "tags".into(),
1305 min: 0.3,
1306 },
1307 Predicate::NumericWithin {
1308 field: "year".into(),
1309 tolerance: 3.0,
1310 },
1311 ]),
1312 ]);
1313 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1314 assert!(
1315 (s - 2.0 / 3.0).abs() < 1e-9,
1316 "expected min(1.0, max(1/3, 2/3)) = 2/3, got {s}"
1317 );
1318 }
1319
1320 #[test]
1323 fn nested_any_of_all_uses_max_over_min() {
1324 let p = Predicate::Any(vec![
1326 Predicate::All(vec![
1327 Predicate::FieldEqual {
1328 field: "gen".into(),
1329 },
1330 Predicate::NumericWithin {
1331 field: "yr".into(),
1332 tolerance: 4.0,
1333 },
1334 ]),
1335 Predicate::NumericWithin {
1336 field: "yr2".into(),
1337 tolerance: 10.0,
1338 },
1339 ]);
1340
1341 let a: HashMap<_, _> = [
1348 ("gen".to_string(), Value::Str("pop".into())),
1349 ("yr".to_string(), Value::Int(2000)),
1350 ("yr2".to_string(), Value::Int(2000)),
1351 ]
1352 .into();
1353 let b: HashMap<_, _> = [
1354 ("gen".to_string(), Value::Str("pop".into())),
1355 ("yr".to_string(), Value::Float(2001.0)),
1356 ("yr2".to_string(), Value::Float(2005.0)),
1357 ]
1358 .into();
1359 let s_a = eval!(&p, ("a", a) => ("b", b)).unwrap();
1360 assert!(
1361 (s_a - 0.75).abs() < 1e-9,
1362 "Any-of-All scenario A: max(min(1.0,0.75), 0.5) must be 0.75, got {s_a}"
1363 );
1364
1365 let a2: HashMap<_, _> = [
1370 ("gen".to_string(), Value::Str("pop".into())),
1371 ("yr".to_string(), Value::Int(2000)),
1372 ("yr2".to_string(), Value::Int(2000)),
1373 ]
1374 .into();
1375 let c: HashMap<_, _> = [
1376 ("gen".to_string(), Value::Str("rock".into())),
1377 ("yr".to_string(), Value::Float(2001.0)),
1378 ("yr2".to_string(), Value::Float(2001.0)),
1379 ]
1380 .into();
1381 let s_b = eval!(&p, ("a", a2) => ("c", c)).unwrap();
1382 assert!(
1383 (s_b - 0.9).abs() < 1e-9,
1384 "Any-of-All scenario B: max(None, 0.9) must be 0.9, got {s_b}"
1385 );
1386 }
1387
1388 #[test]
1390 fn any_validation_errors() {
1391 let empty = sample_rule(Predicate::Any(vec![]));
1393 let err = empty.validate().unwrap_err();
1394 assert_eq!(err, "any() must have at least one predicate");
1395
1396 fn any_chain(depth: usize) -> Predicate {
1398 if depth == 0 {
1399 Predicate::FieldEqual { field: "f".into() }
1400 } else {
1401 Predicate::Any(vec![any_chain(depth - 1)])
1402 }
1403 }
1404
1405 assert!(
1407 sample_rule(any_chain(4)).validate().is_ok(),
1408 "depth 4 must be valid (at cap)"
1409 );
1410 let too_deep = sample_rule(any_chain(5));
1412 let err = too_deep.validate().unwrap_err();
1413 assert!(
1414 err.contains("nesting depth"),
1415 "error must mention 'nesting depth', got: {err}"
1416 );
1417
1418 let bad_inner = sample_rule(Predicate::Any(vec![Predicate::All(vec![])]));
1420 assert!(bad_inner.validate().is_err());
1421 }
1422
1423 #[test]
1425 fn any_watched_fields_collected() {
1426 let p = Predicate::Any(vec![
1427 Predicate::FieldEqual {
1428 field: "ind".into(),
1429 },
1430 Predicate::NumericWithin {
1431 field: "year".into(),
1432 tolerance: 1.0,
1433 },
1434 ]);
1435 let r = sample_rule(p);
1436 assert!(r.validate().is_ok());
1437 let fields: Vec<_> = r.watched_fields().into_iter().collect();
1438 assert_eq!(fields, vec!["ind".to_string(), "year".to_string()]);
1439 }
1440
1441 #[test]
1442 fn new_predicates_validate_and_watch_fields() {
1443 let num = sample_rule(Predicate::NumericWithin {
1444 field: "year".into(),
1445 tolerance: 2.0,
1446 });
1447 assert!(num.validate().is_ok());
1448 assert_eq!(
1449 num.watched_fields().into_iter().collect::<Vec<_>>(),
1450 vec!["year".to_string()]
1451 );
1452 let geo = sample_rule(Predicate::GeoRadius {
1453 field: "loc".into(),
1454 km: 400.0,
1455 });
1456 assert!(geo.validate().is_ok());
1457 let vecp = sample_rule(Predicate::VectorSimilar {
1458 field: "emb".into(),
1459 min: 0.9,
1460 });
1461 assert!(vecp.validate().is_ok());
1462
1463 let mut bad = num.clone();
1464 bad.predicate = Predicate::NumericWithin {
1465 field: "year".into(),
1466 tolerance: -1.0,
1467 };
1468 assert!(bad.validate().is_err());
1469 bad.predicate = Predicate::NumericWithin {
1470 field: "year".into(),
1471 tolerance: f64::NAN,
1472 };
1473 assert!(bad.validate().is_err());
1474
1475 let mut bad_geo = geo;
1476 bad_geo.predicate = Predicate::GeoRadius {
1477 field: "loc".into(),
1478 km: 0.0,
1479 };
1480 assert!(bad_geo.validate().is_err());
1481 bad_geo.predicate = Predicate::GeoRadius {
1482 field: "loc".into(),
1483 km: f64::NAN,
1484 };
1485 assert!(bad_geo.validate().is_err());
1486
1487 let mut bad_vec = vecp;
1488 bad_vec.predicate = Predicate::VectorSimilar {
1489 field: "emb".into(),
1490 min: 0.0,
1491 };
1492 assert!(bad_vec.validate().is_err());
1493 bad_vec.predicate = Predicate::VectorSimilar {
1494 field: "emb".into(),
1495 min: 1.5,
1496 };
1497 assert!(bad_vec.validate().is_err());
1498 }
1499
1500 #[test]
1501 fn default_max_edges_keymatch_is_512_else_32() {
1502 assert_eq!(DEFAULT_SCORED_TOP_K, 32);
1503 assert_eq!(DEFAULT_KEYMATCH_TOP_K, 512);
1505 assert_eq!(DEFAULT_KEYMATCH_TOP_K, MAX_KEYMATCH_LIST as u64);
1506 assert_eq!(
1507 default_max_edges(&Predicate::KeyMatch { field: "fk".into() }),
1508 DEFAULT_KEYMATCH_TOP_K
1509 );
1510 assert_eq!(
1511 default_max_edges(&Predicate::All(vec![Predicate::KeyMatch {
1512 field: "fk".into()
1513 }])),
1514 DEFAULT_KEYMATCH_TOP_K
1515 );
1516 assert_eq!(
1517 default_max_edges(&Predicate::All(vec![Predicate::All(vec![
1518 Predicate::KeyMatch { field: "fk".into() }
1519 ])])),
1520 DEFAULT_KEYMATCH_TOP_K
1521 );
1522 assert_eq!(
1523 default_max_edges(&Predicate::Overlap {
1524 field: "tags".into(),
1525 min: 0.5,
1526 }),
1527 DEFAULT_SCORED_TOP_K
1528 );
1529 assert_eq!(
1530 default_max_edges(&Predicate::Any(vec![Predicate::KeyMatch {
1531 field: "fk".into()
1532 }])),
1533 DEFAULT_SCORED_TOP_K
1534 );
1535 assert!(!is_keymatch_rooted(&Predicate::FieldEqual {
1536 field: "f".into()
1537 }));
1538 }
1539}
1540
1541#[cfg(test)]
1542mod wire_pins {
1543 use super::*;
1544
1545 fn pin(pred: Predicate) -> RuleDef {
1546 RuleDef {
1547 name: "r".into(),
1548 src_label: "A".into(),
1549 dst_label: "B".into(),
1550 predicate: pred,
1551 edge_type: "E".into(),
1552 weight_prop: None,
1553 max_edges: None,
1554 approximate: false,
1555 via_label: None,
1556 via_edge: None,
1557 via_dir: None,
1558 }
1559 }
1560
1561 fn pin_approx(pred: Predicate) -> RuleDef {
1562 RuleDef {
1563 name: "r".into(),
1564 src_label: "A".into(),
1565 dst_label: "B".into(),
1566 predicate: pred,
1567 edge_type: "E".into(),
1568 weight_prop: None,
1569 max_edges: None,
1570 approximate: true,
1571 via_label: None,
1572 via_edge: None,
1573 via_dir: None,
1574 }
1575 }
1576
1577 #[test]
1578 fn old_predicate_variants_keep_encoding() {
1579 assert_eq!(
1586 bincode::serialize(&pin(Predicate::KeyMatch { field: "fk".into() })).unwrap(),
1587 vec![
1588 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,
1589 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,
1590 0, 0, 0, 0
1591 ]
1592 );
1593 assert_eq!(
1594 bincode::serialize(&pin(Predicate::FieldEqual {
1595 field: "ind".into()
1596 }))
1597 .unwrap(),
1598 vec![
1599 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,
1600 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,
1601 0, 0, 0, 0, 0, 0
1602 ]
1603 );
1604 assert_eq!(
1605 bincode::serialize(&pin(Predicate::Overlap {
1606 field: "tags".into(),
1607 min: 0.5,
1608 }))
1609 .unwrap(),
1610 vec![
1611 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,
1612 66, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 116, 97, 103, 115, 0, 0, 0, 0, 0, 0, 224,
1613 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1614 ]
1615 );
1616 assert_eq!(
1617 bincode::serialize(&pin(Predicate::All(vec![
1618 Predicate::KeyMatch { field: "fk".into() },
1619 Predicate::Overlap {
1620 field: "tags".into(),
1621 min: 0.5,
1622 },
1623 ])))
1624 .unwrap(),
1625 vec![
1626 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,
1627 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,
1628 107, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 116, 97, 103, 115, 0, 0, 0, 0, 0, 0, 224,
1629 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1630 ]
1631 );
1632 }
1633
1634 #[test]
1635 fn new_predicate_variants_have_pinned_encoding() {
1636 assert_eq!(
1638 bincode::serialize(&pin(Predicate::NumericWithin {
1639 field: "year".into(),
1640 tolerance: 2.0,
1641 }))
1642 .unwrap(),
1643 vec![
1644 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,
1645 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,
1646 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1647 ]
1648 );
1649 assert_eq!(
1650 bincode::serialize(&pin(Predicate::GeoRadius {
1651 field: "loc".into(),
1652 km: 400.0,
1653 }))
1654 .unwrap(),
1655 vec![
1656 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,
1657 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,
1658 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1659 ]
1660 );
1661 assert_eq!(
1662 bincode::serialize(&pin(Predicate::VectorSimilar {
1663 field: "emb".into(),
1664 min: 0.9,
1665 }))
1666 .unwrap(),
1667 vec![
1668 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,
1669 66, 6, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 101, 109, 98, 205, 204, 204, 204, 204, 204,
1670 236, 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1671 ]
1672 );
1673 }
1674
1675 #[test]
1676 fn any_variant_is_appended_at_discriminant_7() {
1677 let any_fe = pin(Predicate::Any(vec![Predicate::FieldEqual {
1693 field: "f".into(),
1694 }]));
1695 assert_eq!(
1696 bincode::serialize(&any_fe).unwrap(),
1697 vec![
1698 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,
1699 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,
1700 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0,
1701 ],
1702 "Any([FieldEqual{{f}}]) exact-bytes pin failed — discriminant or field layout changed"
1703 );
1704 let decoded: RuleDef = bincode::deserialize(&bincode::serialize(&any_fe).unwrap()).unwrap();
1706 assert_eq!(decoded, any_fe, "Any must round-trip via bincode");
1707 let vs = pin(Predicate::VectorSimilar {
1710 field: "emb".into(),
1711 min: 0.9,
1712 });
1713 let vs_bytes = bincode::serialize(&vs).unwrap();
1714 let vs_decoded: RuleDef = bincode::deserialize(&vs_bytes).unwrap();
1715 assert_eq!(
1716 vs_decoded.predicate,
1717 Predicate::VectorSimilar {
1718 field: "emb".into(),
1719 min: 0.9
1720 },
1721 "VectorSimilar record must still decode after via fields appended"
1722 );
1723 }
1724
1725 #[test]
1726 fn approximate_variant_has_pinned_encoding() {
1727 assert_eq!(
1731 bincode::serialize(&pin_approx(Predicate::VectorSimilar {
1732 field: "emb".into(),
1733 min: 0.9,
1734 }))
1735 .unwrap(),
1736 vec![
1737 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,
1738 66, 6, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 101, 109, 98, 205, 204, 204, 204, 204, 204,
1739 236, 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 1, 0, 0, 0
1740 ]
1741 );
1742 let exact = bincode::serialize(&pin(Predicate::VectorSimilar {
1745 field: "emb".into(),
1746 min: 0.9,
1747 }))
1748 .unwrap();
1749 let approx = bincode::serialize(&pin_approx(Predicate::VectorSimilar {
1750 field: "emb".into(),
1751 min: 0.9,
1752 }))
1753 .unwrap();
1754 assert_eq!(exact.len(), approx.len());
1755 let n = exact.len();
1756 assert_eq!(&exact[..n - 4], &approx[..n - 4]);
1758 assert_eq!(exact[n - 4], 0u8, "exact: approximate=false");
1760 assert_eq!(approx[n - 4], 1u8, "approx: approximate=true");
1761 assert_eq!(&exact[n - 3..], &[0u8, 0, 0]);
1763 assert_eq!(&approx[n - 3..], &[0u8, 0, 0]);
1764 }
1765
1766 fn base_legacy() -> LegacyRuleDefNoVia {
1771 LegacyRuleDefNoVia {
1772 name: "r".into(),
1773 src_label: "A".into(),
1774 dst_label: "B".into(),
1775 predicate: Predicate::FieldEqual {
1776 field: "ind".into(),
1777 },
1778 edge_type: "E".into(),
1779 weight_prop: None,
1780 max_edges: Some(10),
1781 approximate: false,
1782 }
1783 }
1784
1785 fn base_current() -> RuleDef {
1786 RuleDef {
1787 name: "r".into(),
1788 src_label: "A".into(),
1789 dst_label: "B".into(),
1790 predicate: Predicate::FieldEqual {
1791 field: "ind".into(),
1792 },
1793 edge_type: "E".into(),
1794 weight_prop: None,
1795 max_edges: Some(10),
1796 approximate: false,
1797 via_label: None,
1798 via_edge: None,
1799 via_dir: None,
1800 }
1801 }
1802
1803 #[test]
1806 fn decode_rule_def_legacy_roundtrip() {
1807 let legacy_bytes = bincode::serialize(&base_legacy()).unwrap();
1808 let got = decode_rule_def(&legacy_bytes).expect("legacy decode must succeed");
1809 assert_eq!(got.name, "r");
1810 assert_eq!(got.src_label, "A");
1811 assert_eq!(got.max_edges, Some(10));
1812 assert!(!got.approximate);
1813 assert!(got.via_label.is_none(), "via_label must default to None");
1814 assert!(got.via_edge.is_none(), "via_edge must default to None");
1815 assert!(got.via_dir.is_none(), "via_dir must default to None");
1816 }
1817
1818 #[test]
1821 fn decode_rule_def_current_shape_roundtrip() {
1822 let current = RuleDef {
1823 via_label: Some("Mid".into()),
1824 via_edge: Some("hop".into()),
1825 via_dir: Some(core_storage::Direction::Out),
1826 ..base_current()
1827 };
1828 let bytes = bincode::serialize(¤t).unwrap();
1829 let got = decode_rule_def(&bytes).expect("current-shape decode must succeed");
1830 assert_eq!(got, current);
1831 }
1832
1833 #[test]
1835 fn decode_rule_def_garbage_returns_err() {
1836 let garbage = b"\xde\xad\xbe\xef\x00\x00\x00\x00";
1837 let err = decode_rule_def(garbage).unwrap_err();
1838 assert!(
1839 err.contains("current-shape"),
1840 "error must name current-shape attempt: {err}"
1841 );
1842 assert!(
1843 err.contains("legacy-shape"),
1844 "error must name legacy-shape attempt: {err}"
1845 );
1846 }
1847
1848 #[test]
1854 fn decode_rule_def_current_none_via_not_misidentified_as_legacy() {
1855 let current = base_current(); let legacy = base_legacy();
1857 let current_bytes = bincode::serialize(¤t).unwrap();
1858 let legacy_bytes = bincode::serialize(&legacy).unwrap();
1859
1860 assert_ne!(
1862 current_bytes, legacy_bytes,
1863 "current and legacy encodings must not be byte-identical"
1864 );
1865 assert_eq!(
1866 current_bytes.len(),
1867 legacy_bytes.len() + 3,
1868 "current is exactly 3 bytes longer (three Option::None fields)"
1869 );
1870
1871 let from_current =
1873 decode_rule_def(¤t_bytes).expect("current-shape must decode via current path");
1874 let from_legacy =
1875 decode_rule_def(&legacy_bytes).expect("legacy bytes must decode via legacy path");
1876 assert_eq!(
1877 from_current, from_legacy,
1878 "both paths must produce the same RuleDef"
1879 );
1880 assert!(from_current.via_label.is_none());
1881 assert!(from_current.via_edge.is_none());
1882 assert!(from_current.via_dir.is_none());
1883 }
1884}