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 = 1;
189
190pub fn is_keymatch_rooted(p: &Predicate) -> bool {
193 match p {
194 Predicate::KeyMatch { .. } => true,
195 Predicate::All(parts) => !parts.is_empty() && is_keymatch_rooted(&parts[0]),
196 Predicate::Any(_) => false,
197 _ => false,
198 }
199}
200
201pub fn default_max_edges(predicate: &Predicate) -> u64 {
203 if is_keymatch_rooted(predicate) {
204 DEFAULT_KEYMATCH_TOP_K
205 } else {
206 DEFAULT_SCORED_TOP_K
207 }
208}
209
210pub fn predicate_is_vector_similar_rooted(p: &Predicate) -> bool {
214 match p {
215 Predicate::VectorSimilar { .. } => true,
216 Predicate::All(parts) => {
217 !parts.is_empty() && matches!(parts[0], Predicate::VectorSimilar { .. })
218 }
219 Predicate::Any(_) => false,
220 _ => false,
221 }
222}
223
224fn predicate_nesting_depth(p: &Predicate) -> usize {
230 match p {
231 Predicate::All(parts) | Predicate::Any(parts) => {
232 1 + parts.iter().map(predicate_nesting_depth).max().unwrap_or(0)
233 }
234 _ => 0,
235 }
236}
237
238fn validate_pred(p: &Predicate) -> Result<(), String> {
239 match p {
240 Predicate::KeyMatch { field } | Predicate::FieldEqual { field } => {
241 if field.is_empty() {
242 Err("field must not be empty".into())
243 } else {
244 Ok(())
245 }
246 }
247 Predicate::Overlap { field, min } => {
248 if field.is_empty() {
249 Err("field must not be empty".into())
250 } else if !(*min > 0.0 && *min <= 1.0) {
251 Err(format!("overlap min must be in (0,1], got {min}"))
252 } else {
253 Ok(())
254 }
255 }
256 Predicate::NumericWithin { field, tolerance } => {
257 if field.is_empty() {
258 Err("field must not be empty".into())
259 } else if !(tolerance.is_finite() && *tolerance >= 0.0) {
260 Err(format!(
261 "numeric_within tolerance must be finite and >= 0, got {tolerance}"
262 ))
263 } else {
264 Ok(())
265 }
266 }
267 Predicate::GeoRadius { field, km } => {
268 if field.is_empty() {
269 Err("field must not be empty".into())
270 } else if !(km.is_finite() && *km > 0.0) {
271 Err(format!("geo_radius km must be finite and > 0, got {km}"))
272 } else {
273 Ok(())
274 }
275 }
276 Predicate::VectorSimilar { field, min } => {
277 if field.is_empty() {
278 Err("field must not be empty".into())
279 } else if !(*min > 0.0 && *min <= 1.0) {
280 Err(format!("vector_similar min must be in (0,1], got {min}"))
281 } else {
282 Ok(())
283 }
284 }
285 Predicate::All(parts) => {
286 if parts.is_empty() {
287 return Err("all() must have at least one predicate".into());
288 }
289 parts.iter().try_for_each(validate_pred)
290 }
291 Predicate::Any(parts) => {
292 if parts.is_empty() {
293 return Err("any() must have at least one predicate".into());
294 }
295 parts.iter().try_for_each(validate_pred)
296 }
297 }
298}
299
300fn collect_fields(p: &Predicate, out: &mut BTreeSet<String>) {
301 match p {
302 Predicate::KeyMatch { field }
303 | Predicate::FieldEqual { field }
304 | Predicate::Overlap { field, .. }
305 | Predicate::NumericWithin { field, .. }
306 | Predicate::GeoRadius { field, .. }
307 | Predicate::VectorSimilar { field, .. } => {
308 out.insert(field.clone());
309 }
310 Predicate::All(parts) | Predicate::Any(parts) => {
311 parts.iter().for_each(|q| collect_fields(q, out))
312 }
313 }
314}
315
316pub fn evaluate(pred: &Predicate, src: &NodeView, dst: &NodeView) -> Option<f64> {
317 match pred {
318 Predicate::KeyMatch { field } => match (src.props)(field)? {
319 Value::Str(s) if s == dst.key => Some(1.0),
320 _ => None,
321 },
322 Predicate::FieldEqual { field } => {
323 let a = ValueKey::from_value(&(src.props)(field)?)?;
324 let b = ValueKey::from_value(&(dst.props)(field)?)?;
325 (a == b).then_some(1.0)
326 }
327 Predicate::Overlap { field, min } => {
328 let a = list_tokens(&(src.props)(field)?)?;
329 let b = list_tokens(&(dst.props)(field)?)?;
330 let inter = a.intersection(&b).count();
331 let union = a.union(&b).count();
332 if union == 0 || inter == 0 {
333 return None;
334 }
335 let j = inter as f64 / union as f64;
336 (j >= *min).then_some(j)
337 }
338 Predicate::All(parts) => {
339 if parts.is_empty() {
341 return None;
342 }
343 let mut score = f64::INFINITY;
344 for part in parts {
345 score = score.min(evaluate(part, src, dst)?);
346 }
347 Some(score)
348 }
349 Predicate::Any(parts) => {
350 let mut best: Option<f64> = None;
354 for part in parts {
355 if let Some(s) = evaluate(part, src, dst) {
356 best = Some(match best {
357 None => s,
358 Some(prev) => prev.max(s),
359 });
360 }
361 }
362 best
363 }
364 Predicate::NumericWithin { field, tolerance } => {
365 if !tolerance.is_finite() || *tolerance < 0.0 {
369 return None;
370 }
371 let a = as_finite_f64(&(src.props)(field)?)?;
372 let b = as_finite_f64(&(dst.props)(field)?)?;
373 let delta = (a - b).abs();
374 if *tolerance == 0.0 {
375 return (delta == 0.0).then_some(1.0);
376 }
377 (delta <= *tolerance).then_some(1.0 - delta / *tolerance)
378 }
379 Predicate::GeoRadius { field, km } => {
380 if !km.is_finite() || *km <= 0.0 {
381 return None;
382 }
383 let (alat, alon) = as_latlon(&(src.props)(field)?)?;
384 let (blat, blon) = as_latlon(&(dst.props)(field)?)?;
385 let d = haversine_km(alat, alon, blat, blon);
386 if !d.is_finite() {
387 return None;
388 }
389 (d <= *km).then_some(1.0 - d / *km)
390 }
391 Predicate::VectorSimilar { field, min } => {
392 let a = as_numeric_list(&(src.props)(field)?)?;
393 let b = as_numeric_list(&(dst.props)(field)?)?;
394 if a.len() != b.len() {
395 return None;
396 }
397 let cos = cosine(&a, &b)?.min(1.0);
398 (cos >= *min).then_some(cos)
399 }
400 }
401}
402
403fn as_finite_f64(v: &Value) -> Option<f64> {
404 match v {
405 Value::Int(i) => Some(*i as f64),
406 Value::Float(f) if f.is_finite() => Some(*f),
407 _ => None,
408 }
409}
410
411fn as_latlon(v: &Value) -> Option<(f64, f64)> {
412 let Value::List(items) = v else {
413 return None;
414 };
415 if items.len() != 2 {
416 return None;
417 }
418 let lat = as_finite_f64(&items[0])?;
419 let lon = as_finite_f64(&items[1])?;
420 if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon) {
421 Some((lat, lon))
422 } else {
423 None
424 }
425}
426
427fn as_numeric_list(v: &Value) -> Option<Vec<f64>> {
428 let Value::List(items) = v else {
429 return None;
430 };
431 if items.is_empty() {
432 return None;
433 }
434 items.iter().map(as_finite_f64).collect()
435}
436
437const EARTH_RADIUS_KM: f64 = 6371.0088;
439
440fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
441 let phi1 = lat1.to_radians();
442 let phi2 = lat2.to_radians();
443 let dphi = (lat2 - lat1).to_radians();
444 let dlam = (lon2 - lon1).to_radians();
445 let a = ((dphi / 2.0).sin().powi(2) + phi1.cos() * phi2.cos() * (dlam / 2.0).sin().powi(2))
446 .clamp(0.0, 1.0);
447 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
448 EARTH_RADIUS_KM * c
449}
450
451fn cosine(a: &[f64], b: &[f64]) -> Option<f64> {
452 let mut dot = 0.0;
453 let mut na2 = 0.0;
454 let mut nb2 = 0.0;
455 for (x, y) in a.iter().zip(b.iter()) {
456 dot += *x * *y;
457 na2 += *x * *x;
458 nb2 += *y * *y;
459 }
460 let na = na2.sqrt();
461 let nb = nb2.sqrt();
462 if !(na > 0.0 && nb > 0.0) {
463 return None;
464 }
465 let cos = dot / (na * nb);
466 cos.is_finite().then_some(cos)
467}
468
469pub fn cosine_early_exit(
505 a: &[f64],
506 b: &[f64],
507 ckpts_a: &[f64; 8],
508 ckpts_b: &[f64; 8],
509 norm_a: f64,
510 norm_b: f64,
511 min: f64,
512) -> Option<f64> {
513 let dim = a.len();
514 if dim == 0 || dim != b.len() {
515 return None; }
517 let denom = norm_a * norm_b;
518 if !denom.is_finite() || denom == 0.0 {
519 return None;
520 }
521
522 let eps = dim as f64 * f64::EPSILON * 4.0;
528 let mut dot = 0.0f64;
529
530 for ci in 0..8usize {
531 let chunk_start = ci * dim / 8;
532 let chunk_end = if ci < 7 { (ci + 1) * dim / 8 } else { dim };
533 for k in chunk_start..chunk_end {
534 dot += a[k] * b[k];
535 }
536 if ci < 7 {
540 let bound = ckpts_a[ci + 1] * ckpts_b[ci + 1];
541 let cos_max = (dot + bound) / denom;
542 if cos_max.is_finite() && cos_max < min - eps {
543 return None;
544 }
545 }
546 }
547
548 let cos = (dot / denom).min(1.0);
550 if cos.is_finite() && cos >= min {
551 Some(cos)
552 } else {
553 None
554 }
555}
556
557#[derive(serde::Serialize, serde::Deserialize)]
571struct LegacyRuleDefNoVia {
572 name: String,
573 src_label: String,
574 dst_label: String,
575 predicate: Predicate,
576 edge_type: String,
577 weight_prop: Option<String>,
578 max_edges: Option<u64>,
579 approximate: bool,
580}
581
582pub fn decode_rule_def(bytes: &[u8]) -> Result<RuleDef, String> {
599 use bincode::Options as _;
600 let opts = bincode::options()
606 .with_fixint_encoding()
607 .with_no_limit()
608 .reject_trailing_bytes();
609
610 match opts.deserialize::<RuleDef>(bytes) {
612 Ok(def) => Ok(def),
613 Err(current_err) => {
614 match opts.deserialize::<LegacyRuleDefNoVia>(bytes) {
616 Ok(legacy) => Ok(RuleDef {
617 name: legacy.name,
618 src_label: legacy.src_label,
619 dst_label: legacy.dst_label,
620 predicate: legacy.predicate,
621 edge_type: legacy.edge_type,
622 weight_prop: legacy.weight_prop,
623 max_edges: legacy.max_edges,
624 approximate: legacy.approximate,
625 via_label: None,
626 via_edge: None,
627 via_dir: None,
628 }),
629 Err(legacy_err) => Err(format!(
630 "corrupt rule_def — current-shape: {current_err}; \
631 legacy-shape (pre-0.1.2 no-via): {legacy_err}"
632 )),
633 }
634 }
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use core_storage::Value;
642 use std::collections::HashMap;
643
644 macro_rules! eval {
650 ($p:expr, ($sk:expr, $sm:ident) => ($dk:expr, $dm:ident)) => {{
651 let sp = |f: &str| $sm.get(f).cloned();
652 let dp = |f: &str| $dm.get(f).cloned();
653 evaluate(
654 $p,
655 &NodeView {
656 key: $sk,
657 props: &sp,
658 },
659 &NodeView {
660 key: $dk,
661 props: &dp,
662 },
663 )
664 }};
665 }
666
667 #[test]
668 fn key_match_links_fk_to_key() {
669 let s: HashMap<_, _> = [("cid".to_string(), Value::Str("c1".into()))].into();
670 let d: HashMap<String, Value> = HashMap::new();
671 let p = Predicate::KeyMatch {
672 field: "cid".into(),
673 };
674 assert_eq!(eval!(&p, ("t1", s) => ("c1", d)), Some(1.0));
675 assert_eq!(eval!(&p, ("t1", s) => ("c2", d)), None);
676 assert_eq!(eval!(&p, ("t1", d) => ("c1", d)), None); }
678
679 #[test]
680 fn field_equal_needs_both_scalars_equal() {
681 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
682 let b = a.clone();
683 let c: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
684 let p = Predicate::FieldEqual {
685 field: "ind".into(),
686 };
687 assert_eq!(eval!(&p, ("a", a) => ("b", b)), Some(1.0));
688 assert_eq!(eval!(&p, ("a", a) => ("c", c)), None);
689 }
690
691 #[test]
692 fn overlap_is_jaccard_with_threshold() {
693 let mk =
694 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
695 let a: HashMap<_, _> = [("tags".to_string(), mk(&["x", "y"]))].into();
696 let b: HashMap<_, _> = [("tags".to_string(), mk(&["y", "z"]))].into();
697 let p = Predicate::Overlap {
698 field: "tags".into(),
699 min: 0.3,
700 };
701 let score = eval!(&p, ("a", a) => ("b", b)).unwrap();
703 assert!((score - 1.0 / 3.0).abs() < 1e-9);
704 let strict = Predicate::Overlap {
705 field: "tags".into(),
706 min: 0.5,
707 };
708 assert_eq!(eval!(&strict, ("a", a) => ("b", b)), None);
709 let e: HashMap<_, _> = [("tags".to_string(), mk(&[]))].into();
711 assert_eq!(eval!(&p, ("a", e) => ("b", b)), None);
712 }
713
714 #[test]
715 fn all_takes_min_score_and_requires_every_part() {
716 let mk =
717 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
718 let a: HashMap<_, _> = [
719 ("ind".to_string(), Value::Str("arch".into())),
720 ("tags".to_string(), mk(&["x", "y"])),
721 ]
722 .into();
723 let b: HashMap<_, _> = [
724 ("ind".to_string(), Value::Str("arch".into())),
725 ("tags".to_string(), mk(&["y"])),
726 ]
727 .into();
728 let p = Predicate::All(vec![
729 Predicate::FieldEqual {
730 field: "ind".into(),
731 },
732 Predicate::Overlap {
733 field: "tags".into(),
734 min: 0.4,
735 },
736 ]);
737 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
738 assert!((s - 0.5).abs() < 1e-9); }
740
741 #[test]
742 fn validation_rejects_bad_rules_and_collects_watched_fields() {
743 let ok = RuleDef {
744 name: "r".into(),
745 src_label: "A".into(),
746 dst_label: "B".into(),
747 predicate: Predicate::All(vec![
748 Predicate::KeyMatch { field: "fk".into() },
749 Predicate::Overlap {
750 field: "tags".into(),
751 min: 0.5,
752 },
753 ]),
754 edge_type: "E".into(),
755 weight_prop: Some("score".into()),
756 max_edges: None,
757 approximate: false,
758 via_label: None,
759 via_edge: None,
760 via_dir: None,
761 };
762 assert!(ok.validate().is_ok());
763 assert_eq!(
764 ok.watched_fields().into_iter().collect::<Vec<_>>(),
765 vec!["fk".to_string(), "tags".to_string()]
766 );
767 let mut bad = ok.clone();
768 bad.predicate = Predicate::Overlap {
769 field: "t".into(),
770 min: 0.0,
771 };
772 assert!(bad.validate().is_err()); let mut bad2 = ok.clone();
774 bad2.edge_type = String::new();
775 assert!(bad2.validate().is_err());
776 let mut bad3 = ok;
777 bad3.predicate = Predicate::All(vec![]);
778 assert!(bad3.validate().is_err());
779 }
780
781 #[test]
782 fn evaluate_empty_all_returns_none() {
783 let empty: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
784 let sp = |f: &str| empty.get(f).cloned();
785 let dp = |f: &str| empty.get(f).cloned();
786 let src = NodeView {
787 key: "a",
788 props: &sp,
789 };
790 let dst = NodeView {
791 key: "b",
792 props: &dp,
793 };
794 assert_eq!(evaluate(&Predicate::All(vec![]), &src, &dst), None);
795 }
796
797 #[test]
798 fn numeric_within_int_float_cross_type() {
799 let a: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
800 let b: HashMap<_, _> = [("year".to_string(), Value::Float(2000.0))].into();
801 let tight = Predicate::NumericWithin {
802 field: "year".into(),
803 tolerance: 2.0,
804 };
805 assert_eq!(eval!(&tight, ("a", a) => ("b", b)), Some(0.0));
807 let loose = Predicate::NumericWithin {
808 field: "year".into(),
809 tolerance: 3.0,
810 };
811 let score = eval!(&loose, ("a", a) => ("b", b)).unwrap();
812 assert!((score - 1.0 / 3.0).abs() < 1e-9);
813 }
814
815 #[test]
816 fn numeric_within_missing_or_non_numeric_is_none() {
817 let num: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
818 let missing: HashMap<String, Value> = HashMap::new();
819 let text: HashMap<_, _> = [("year".to_string(), Value::Str("1998".into()))].into();
820 let p = Predicate::NumericWithin {
821 field: "year".into(),
822 tolerance: 2.0,
823 };
824 assert_eq!(eval!(&p, ("a", num) => ("b", missing)), None);
825 assert_eq!(eval!(&p, ("a", missing) => ("b", num)), None);
826 assert_eq!(eval!(&p, ("a", num) => ("b", text)), None);
827 }
828
829 #[test]
830 fn numeric_within_tol_zero_requires_exact() {
831 let a: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
832 let same: HashMap<_, _> = [("year".to_string(), Value::Float(1998.0))].into();
833 let other: HashMap<_, _> = [("year".to_string(), Value::Int(1999))].into();
834 let p = Predicate::NumericWithin {
835 field: "year".into(),
836 tolerance: 0.0,
837 };
838 assert_eq!(eval!(&p, ("a", a) => ("b", same)), Some(1.0));
839 assert_eq!(eval!(&p, ("a", a) => ("b", other)), None);
840 }
841
842 #[test]
843 fn numeric_within_non_finite_is_none() {
844 let a: HashMap<_, _> = [("year".to_string(), Value::Float(f64::NAN))].into();
845 let b: HashMap<_, _> = [("year".to_string(), Value::Float(1.0))].into();
846 let inf: HashMap<_, _> = [("year".to_string(), Value::Float(f64::INFINITY))].into();
847 let p = Predicate::NumericWithin {
848 field: "year".into(),
849 tolerance: 2.0,
850 };
851 assert_eq!(eval!(&p, ("a", a) => ("b", b)), None);
852 assert_eq!(eval!(&p, ("a", inf) => ("b", b)), None);
853 }
854
855 fn geo_pair(
856 src: (f64, f64),
857 dst: (f64, f64),
858 ) -> (HashMap<String, Value>, HashMap<String, Value>) {
859 let mk = |lat: f64, lon: f64| {
860 let mut m = HashMap::new();
861 m.insert(
862 "loc".to_string(),
863 Value::List(vec![Value::Float(lat), Value::Float(lon)]),
864 );
865 m
866 };
867 (mk(src.0, src.1), mk(dst.0, dst.1))
868 }
869
870 #[test]
871 fn geo_radius_paris_london() {
872 let (paris, london) = geo_pair((48.8566, 2.3522), (51.5074, -0.1278));
874 let inside = Predicate::GeoRadius {
875 field: "loc".into(),
876 km: 400.0,
877 };
878 let score = eval!(&inside, ("p", paris) => ("l", london)).unwrap();
879 assert!((score - 0.14125).abs() < 0.001);
881 let outside = Predicate::GeoRadius {
882 field: "loc".into(),
883 km: 300.0,
884 };
885 assert_eq!(eval!(&outside, ("p", paris) => ("l", london)), None);
886 }
887
888 #[test]
889 fn geo_radius_identical_coordinates_score_one() {
890 let (a, b) = geo_pair((48.8566, 2.3522), (48.8566, 2.3522));
891 let p = Predicate::GeoRadius {
892 field: "loc".into(),
893 km: 400.0,
894 };
895 assert_eq!(eval!(&p, ("a", a) => ("b", b)), Some(1.0));
896 }
897
898 #[test]
899 fn geo_radius_malformed_is_none() {
900 let paris: HashMap<_, _> = [(
901 "loc".to_string(),
902 Value::List(vec![Value::Float(48.8566), Value::Float(2.3522)]),
903 )]
904 .into();
905 let one: HashMap<_, _> =
906 [("loc".to_string(), Value::List(vec![Value::Float(48.8566)]))].into();
907 let three: HashMap<_, _> = [(
908 "loc".to_string(),
909 Value::List(vec![
910 Value::Float(48.8566),
911 Value::Float(2.3522),
912 Value::Float(0.0),
913 ]),
914 )]
915 .into();
916 let string_el: HashMap<_, _> = [(
917 "loc".to_string(),
918 Value::List(vec![Value::Str("48.8566".into()), Value::Float(2.3522)]),
919 )]
920 .into();
921 let lat91: HashMap<_, _> = [(
922 "loc".to_string(),
923 Value::List(vec![Value::Float(91.0), Value::Float(0.0)]),
924 )]
925 .into();
926 let p = Predicate::GeoRadius {
927 field: "loc".into(),
928 km: 400.0,
929 };
930 assert_eq!(eval!(&p, ("a", paris) => ("b", one)), None);
931 assert_eq!(eval!(&p, ("a", paris) => ("b", three)), None);
932 assert_eq!(eval!(&p, ("a", paris) => ("b", string_el)), None);
933 assert_eq!(eval!(&p, ("a", paris) => ("b", lat91)), None);
934 }
935
936 fn vec_field(vals: &[f64]) -> HashMap<String, Value> {
937 [(
938 "emb".to_string(),
939 Value::List(vals.iter().copied().map(Value::Float).collect()),
940 )]
941 .into()
942 }
943
944 #[test]
945 fn vector_similar_cosine_and_rejects() {
946 let a = vec_field(&[1.0, 0.0]);
947 let same = vec_field(&[1.0, 0.0]);
948 let ortho = vec_field(&[0.0, 1.0]);
949 let p = Predicate::VectorSimilar {
950 field: "emb".into(),
951 min: 0.5,
952 };
953 assert_eq!(eval!(&p, ("a", a) => ("b", same)), Some(1.0));
954 assert_eq!(eval!(&p, ("a", a) => ("b", ortho)), None); let u = vec_field(&[1.0, 2.0]);
957 let scaled = vec_field(&[2.0, 4.0]);
958 let score = eval!(&p, ("a", u) => ("b", scaled)).unwrap();
959 assert!((1.0 - score).abs() < 1e-9); let dim3 = vec_field(&[1.0, 0.0, 0.0]);
962 assert_eq!(eval!(&p, ("a", a) => ("b", dim3)), None);
963 let zero = vec_field(&[0.0, 0.0]);
964 assert_eq!(eval!(&p, ("a", a) => ("b", zero)), None);
965 }
966
967 #[test]
968 fn approximate_only_valid_with_vector_similar_rooted_predicate() {
969 let ok_vec = RuleDef {
971 name: "av".into(),
972 src_label: "V".into(),
973 dst_label: "V".into(),
974 predicate: Predicate::VectorSimilar {
975 field: "emb".into(),
976 min: 0.9,
977 },
978 edge_type: "VEC".into(),
979 weight_prop: None,
980 max_edges: None,
981 approximate: true,
982 via_label: None,
983 via_edge: None,
984 via_dir: None,
985 };
986 assert!(ok_vec.validate().is_ok());
987
988 let ok_all = RuleDef {
990 name: "av2".into(),
991 src_label: "V".into(),
992 dst_label: "V".into(),
993 predicate: Predicate::All(vec![
994 Predicate::VectorSimilar {
995 field: "emb".into(),
996 min: 0.9,
997 },
998 Predicate::FieldEqual {
999 field: "kind".into(),
1000 },
1001 ]),
1002 edge_type: "VEC2".into(),
1003 weight_prop: None,
1004 max_edges: None,
1005 approximate: true,
1006 via_label: None,
1007 via_edge: None,
1008 via_dir: None,
1009 };
1010 assert!(ok_all.validate().is_ok());
1011
1012 let bad_fe = RuleDef {
1014 name: "bfe".into(),
1015 src_label: "A".into(),
1016 dst_label: "A".into(),
1017 predicate: Predicate::FieldEqual { field: "f".into() },
1018 edge_type: "FE".into(),
1019 weight_prop: None,
1020 max_edges: None,
1021 approximate: true,
1022 via_label: None,
1023 via_edge: None,
1024 via_dir: None,
1025 };
1026 assert!(bad_fe.validate().is_err());
1027
1028 let bad_ov = RuleDef {
1030 name: "bov".into(),
1031 src_label: "A".into(),
1032 dst_label: "A".into(),
1033 predicate: Predicate::Overlap {
1034 field: "tags".into(),
1035 min: 0.5,
1036 },
1037 edge_type: "OV".into(),
1038 weight_prop: None,
1039 max_edges: None,
1040 approximate: true,
1041 via_label: None,
1042 via_edge: None,
1043 via_dir: None,
1044 };
1045 assert!(bad_ov.validate().is_err());
1046
1047 let bad_all_order = RuleDef {
1049 name: "bao".into(),
1050 src_label: "A".into(),
1051 dst_label: "A".into(),
1052 predicate: Predicate::All(vec![
1053 Predicate::FieldEqual { field: "f".into() },
1054 Predicate::VectorSimilar {
1055 field: "emb".into(),
1056 min: 0.9,
1057 },
1058 ]),
1059 edge_type: "E".into(),
1060 weight_prop: None,
1061 max_edges: None,
1062 approximate: true,
1063 via_label: None,
1064 via_edge: None,
1065 via_dir: None,
1066 };
1067 assert!(bad_all_order.validate().is_err());
1068 }
1069
1070 #[test]
1071 fn validate_rejects_via_with_approximate() {
1072 let bad = RuleDef {
1074 name: "vbad".into(),
1075 src_label: "A".into(),
1076 dst_label: "B".into(),
1077 predicate: Predicate::VectorSimilar {
1078 field: "emb".into(),
1079 min: 0.9,
1080 },
1081 edge_type: "VEC".into(),
1082 weight_prop: None,
1083 max_edges: None,
1084 approximate: true,
1085 via_label: Some("Mid".into()),
1086 via_edge: Some("hop".into()),
1087 via_dir: None,
1088 };
1089 let err = bad.validate().unwrap_err();
1090 assert_eq!(err, "via-hop rules do not support approximate: true");
1091
1092 let ok = RuleDef {
1094 approximate: false,
1095 ..bad.clone()
1096 };
1097 assert!(ok.validate().is_ok());
1098 }
1099
1100 #[test]
1101 fn all_composes_field_equal_and_numeric_within() {
1102 let a: HashMap<_, _> = [
1103 ("ind".to_string(), Value::Str("arch".into())),
1104 ("year".to_string(), Value::Int(1998)),
1105 ]
1106 .into();
1107 let b: HashMap<_, _> = [
1108 ("ind".to_string(), Value::Str("arch".into())),
1109 ("year".to_string(), Value::Float(2000.0)),
1110 ]
1111 .into();
1112 let p = Predicate::All(vec![
1113 Predicate::FieldEqual {
1114 field: "ind".into(),
1115 },
1116 Predicate::NumericWithin {
1117 field: "year".into(),
1118 tolerance: 3.0,
1119 },
1120 ]);
1121 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1122 assert!((s - 1.0 / 3.0).abs() < 1e-9); }
1124
1125 fn sample_rule(pred: Predicate) -> RuleDef {
1126 RuleDef {
1127 name: "r".into(),
1128 src_label: "A".into(),
1129 dst_label: "B".into(),
1130 predicate: pred,
1131 edge_type: "E".into(),
1132 weight_prop: None,
1133 max_edges: None,
1134 approximate: false,
1135 via_label: None,
1136 via_edge: None,
1137 via_dir: None,
1138 }
1139 }
1140
1141 #[test]
1147 fn any_takes_max_score_and_requires_at_least_one_branch() {
1148 let mk =
1149 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1150 let a: HashMap<_, _> = [
1155 ("ind".to_string(), Value::Str("arch".into())),
1156 ("tags".to_string(), mk(&["x", "y"])),
1157 ]
1158 .into();
1159 let b: HashMap<_, _> = [
1160 ("ind".to_string(), Value::Str("law".into())),
1161 ("tags".to_string(), mk(&["y", "z"])),
1162 ]
1163 .into();
1164 let p = Predicate::Any(vec![
1165 Predicate::FieldEqual {
1166 field: "ind".into(),
1167 },
1168 Predicate::Overlap {
1169 field: "tags".into(),
1170 min: 0.3,
1171 },
1172 ]);
1173 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1174 assert!(
1175 (s - 1.0 / 3.0).abs() < 1e-9,
1176 "score must be max(None, 1/3) = 1/3, got {s}"
1177 );
1178 }
1179
1180 #[test]
1182 fn any_score_is_max_when_both_branches_match() {
1183 let a: HashMap<_, _> = [
1188 ("ind".to_string(), Value::Str("arch".into())),
1189 ("year".to_string(), Value::Int(2000)),
1190 ]
1191 .into();
1192 let b: HashMap<_, _> = [
1193 ("ind".to_string(), Value::Str("arch".into())),
1194 ("year".to_string(), Value::Float(2001.0)),
1195 ]
1196 .into();
1197 let p = Predicate::Any(vec![
1198 Predicate::FieldEqual {
1199 field: "ind".into(),
1200 },
1201 Predicate::NumericWithin {
1202 field: "year".into(),
1203 tolerance: 3.0,
1204 },
1205 ]);
1206 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1207 assert!(
1208 (s - 1.0).abs() < 1e-9,
1209 "score must be max(1.0, 2/3) = 1.0, got {s}"
1210 );
1211 }
1212
1213 #[test]
1215 fn any_returns_none_when_all_branches_fail() {
1216 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
1217 let b: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
1218 let p = Predicate::Any(vec![
1219 Predicate::FieldEqual {
1220 field: "ind".into(),
1221 },
1222 Predicate::FieldEqual {
1223 field: "ind".into(),
1224 },
1225 ]);
1226 assert_eq!(eval!(&p, ("a", a) => ("b", b)), None);
1227 }
1228
1229 #[test]
1232 fn nested_all_of_any_uses_min_over_max() {
1233 let mk =
1234 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1235 let a: HashMap<_, _> = [
1243 ("ind".to_string(), Value::Str("arch".into())),
1244 ("tags".to_string(), mk(&["x", "y"])),
1245 ("year".to_string(), Value::Int(2000)),
1246 ]
1247 .into();
1248 let b: HashMap<_, _> = [
1249 ("ind".to_string(), Value::Str("arch".into())),
1250 ("tags".to_string(), mk(&["y", "z"])),
1251 ("year".to_string(), Value::Float(2001.0)),
1252 ]
1253 .into();
1254 let p = Predicate::All(vec![
1255 Predicate::FieldEqual {
1256 field: "ind".into(),
1257 },
1258 Predicate::Any(vec![
1259 Predicate::Overlap {
1260 field: "tags".into(),
1261 min: 0.3,
1262 },
1263 Predicate::NumericWithin {
1264 field: "year".into(),
1265 tolerance: 3.0,
1266 },
1267 ]),
1268 ]);
1269 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1270 assert!(
1271 (s - 2.0 / 3.0).abs() < 1e-9,
1272 "expected min(1.0, max(1/3, 2/3)) = 2/3, got {s}"
1273 );
1274 }
1275
1276 #[test]
1279 fn nested_any_of_all_uses_max_over_min() {
1280 let p = Predicate::Any(vec![
1282 Predicate::All(vec![
1283 Predicate::FieldEqual {
1284 field: "gen".into(),
1285 },
1286 Predicate::NumericWithin {
1287 field: "yr".into(),
1288 tolerance: 4.0,
1289 },
1290 ]),
1291 Predicate::NumericWithin {
1292 field: "yr2".into(),
1293 tolerance: 10.0,
1294 },
1295 ]);
1296
1297 let a: HashMap<_, _> = [
1304 ("gen".to_string(), Value::Str("pop".into())),
1305 ("yr".to_string(), Value::Int(2000)),
1306 ("yr2".to_string(), Value::Int(2000)),
1307 ]
1308 .into();
1309 let b: HashMap<_, _> = [
1310 ("gen".to_string(), Value::Str("pop".into())),
1311 ("yr".to_string(), Value::Float(2001.0)),
1312 ("yr2".to_string(), Value::Float(2005.0)),
1313 ]
1314 .into();
1315 let s_a = eval!(&p, ("a", a) => ("b", b)).unwrap();
1316 assert!(
1317 (s_a - 0.75).abs() < 1e-9,
1318 "Any-of-All scenario A: max(min(1.0,0.75), 0.5) must be 0.75, got {s_a}"
1319 );
1320
1321 let a2: HashMap<_, _> = [
1326 ("gen".to_string(), Value::Str("pop".into())),
1327 ("yr".to_string(), Value::Int(2000)),
1328 ("yr2".to_string(), Value::Int(2000)),
1329 ]
1330 .into();
1331 let c: HashMap<_, _> = [
1332 ("gen".to_string(), Value::Str("rock".into())),
1333 ("yr".to_string(), Value::Float(2001.0)),
1334 ("yr2".to_string(), Value::Float(2001.0)),
1335 ]
1336 .into();
1337 let s_b = eval!(&p, ("a", a2) => ("c", c)).unwrap();
1338 assert!(
1339 (s_b - 0.9).abs() < 1e-9,
1340 "Any-of-All scenario B: max(None, 0.9) must be 0.9, got {s_b}"
1341 );
1342 }
1343
1344 #[test]
1346 fn any_validation_errors() {
1347 let empty = sample_rule(Predicate::Any(vec![]));
1349 let err = empty.validate().unwrap_err();
1350 assert_eq!(err, "any() must have at least one predicate");
1351
1352 fn any_chain(depth: usize) -> Predicate {
1354 if depth == 0 {
1355 Predicate::FieldEqual { field: "f".into() }
1356 } else {
1357 Predicate::Any(vec![any_chain(depth - 1)])
1358 }
1359 }
1360
1361 assert!(
1363 sample_rule(any_chain(4)).validate().is_ok(),
1364 "depth 4 must be valid (at cap)"
1365 );
1366 let too_deep = sample_rule(any_chain(5));
1368 let err = too_deep.validate().unwrap_err();
1369 assert!(
1370 err.contains("nesting depth"),
1371 "error must mention 'nesting depth', got: {err}"
1372 );
1373
1374 let bad_inner = sample_rule(Predicate::Any(vec![Predicate::All(vec![])]));
1376 assert!(bad_inner.validate().is_err());
1377 }
1378
1379 #[test]
1381 fn any_watched_fields_collected() {
1382 let p = Predicate::Any(vec![
1383 Predicate::FieldEqual {
1384 field: "ind".into(),
1385 },
1386 Predicate::NumericWithin {
1387 field: "year".into(),
1388 tolerance: 1.0,
1389 },
1390 ]);
1391 let r = sample_rule(p);
1392 assert!(r.validate().is_ok());
1393 let fields: Vec<_> = r.watched_fields().into_iter().collect();
1394 assert_eq!(fields, vec!["ind".to_string(), "year".to_string()]);
1395 }
1396
1397 #[test]
1398 fn new_predicates_validate_and_watch_fields() {
1399 let num = sample_rule(Predicate::NumericWithin {
1400 field: "year".into(),
1401 tolerance: 2.0,
1402 });
1403 assert!(num.validate().is_ok());
1404 assert_eq!(
1405 num.watched_fields().into_iter().collect::<Vec<_>>(),
1406 vec!["year".to_string()]
1407 );
1408 let geo = sample_rule(Predicate::GeoRadius {
1409 field: "loc".into(),
1410 km: 400.0,
1411 });
1412 assert!(geo.validate().is_ok());
1413 let vecp = sample_rule(Predicate::VectorSimilar {
1414 field: "emb".into(),
1415 min: 0.9,
1416 });
1417 assert!(vecp.validate().is_ok());
1418
1419 let mut bad = num.clone();
1420 bad.predicate = Predicate::NumericWithin {
1421 field: "year".into(),
1422 tolerance: -1.0,
1423 };
1424 assert!(bad.validate().is_err());
1425 bad.predicate = Predicate::NumericWithin {
1426 field: "year".into(),
1427 tolerance: f64::NAN,
1428 };
1429 assert!(bad.validate().is_err());
1430
1431 let mut bad_geo = geo;
1432 bad_geo.predicate = Predicate::GeoRadius {
1433 field: "loc".into(),
1434 km: 0.0,
1435 };
1436 assert!(bad_geo.validate().is_err());
1437 bad_geo.predicate = Predicate::GeoRadius {
1438 field: "loc".into(),
1439 km: f64::NAN,
1440 };
1441 assert!(bad_geo.validate().is_err());
1442
1443 let mut bad_vec = vecp;
1444 bad_vec.predicate = Predicate::VectorSimilar {
1445 field: "emb".into(),
1446 min: 0.0,
1447 };
1448 assert!(bad_vec.validate().is_err());
1449 bad_vec.predicate = Predicate::VectorSimilar {
1450 field: "emb".into(),
1451 min: 1.5,
1452 };
1453 assert!(bad_vec.validate().is_err());
1454 }
1455
1456 #[test]
1457 fn default_max_edges_keymatch_is_1_else_32() {
1458 assert_eq!(DEFAULT_SCORED_TOP_K, 32);
1459 assert_eq!(DEFAULT_KEYMATCH_TOP_K, 1);
1460 assert_eq!(
1461 default_max_edges(&Predicate::KeyMatch { field: "fk".into() }),
1462 DEFAULT_KEYMATCH_TOP_K
1463 );
1464 assert_eq!(
1465 default_max_edges(&Predicate::All(vec![Predicate::KeyMatch {
1466 field: "fk".into()
1467 }])),
1468 DEFAULT_KEYMATCH_TOP_K
1469 );
1470 assert_eq!(
1471 default_max_edges(&Predicate::All(vec![Predicate::All(vec![
1472 Predicate::KeyMatch { field: "fk".into() }
1473 ])])),
1474 DEFAULT_KEYMATCH_TOP_K
1475 );
1476 assert_eq!(
1477 default_max_edges(&Predicate::Overlap {
1478 field: "tags".into(),
1479 min: 0.5,
1480 }),
1481 DEFAULT_SCORED_TOP_K
1482 );
1483 assert_eq!(
1484 default_max_edges(&Predicate::Any(vec![Predicate::KeyMatch {
1485 field: "fk".into()
1486 }])),
1487 DEFAULT_SCORED_TOP_K
1488 );
1489 assert!(!is_keymatch_rooted(&Predicate::FieldEqual {
1490 field: "f".into()
1491 }));
1492 }
1493}
1494
1495#[cfg(test)]
1496mod wire_pins {
1497 use super::*;
1498
1499 fn pin(pred: Predicate) -> RuleDef {
1500 RuleDef {
1501 name: "r".into(),
1502 src_label: "A".into(),
1503 dst_label: "B".into(),
1504 predicate: pred,
1505 edge_type: "E".into(),
1506 weight_prop: None,
1507 max_edges: None,
1508 approximate: false,
1509 via_label: None,
1510 via_edge: None,
1511 via_dir: None,
1512 }
1513 }
1514
1515 fn pin_approx(pred: Predicate) -> RuleDef {
1516 RuleDef {
1517 name: "r".into(),
1518 src_label: "A".into(),
1519 dst_label: "B".into(),
1520 predicate: pred,
1521 edge_type: "E".into(),
1522 weight_prop: None,
1523 max_edges: None,
1524 approximate: true,
1525 via_label: None,
1526 via_edge: None,
1527 via_dir: None,
1528 }
1529 }
1530
1531 #[test]
1532 fn old_predicate_variants_keep_encoding() {
1533 assert_eq!(
1540 bincode::serialize(&pin(Predicate::KeyMatch { field: "fk".into() })).unwrap(),
1541 vec![
1542 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,
1543 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,
1544 0, 0, 0, 0
1545 ]
1546 );
1547 assert_eq!(
1548 bincode::serialize(&pin(Predicate::FieldEqual {
1549 field: "ind".into()
1550 }))
1551 .unwrap(),
1552 vec![
1553 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,
1554 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,
1555 0, 0, 0, 0, 0, 0
1556 ]
1557 );
1558 assert_eq!(
1559 bincode::serialize(&pin(Predicate::Overlap {
1560 field: "tags".into(),
1561 min: 0.5,
1562 }))
1563 .unwrap(),
1564 vec![
1565 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,
1566 66, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 116, 97, 103, 115, 0, 0, 0, 0, 0, 0, 224,
1567 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1568 ]
1569 );
1570 assert_eq!(
1571 bincode::serialize(&pin(Predicate::All(vec![
1572 Predicate::KeyMatch { field: "fk".into() },
1573 Predicate::Overlap {
1574 field: "tags".into(),
1575 min: 0.5,
1576 },
1577 ])))
1578 .unwrap(),
1579 vec![
1580 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,
1581 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,
1582 107, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 116, 97, 103, 115, 0, 0, 0, 0, 0, 0, 224,
1583 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1584 ]
1585 );
1586 }
1587
1588 #[test]
1589 fn new_predicate_variants_have_pinned_encoding() {
1590 assert_eq!(
1592 bincode::serialize(&pin(Predicate::NumericWithin {
1593 field: "year".into(),
1594 tolerance: 2.0,
1595 }))
1596 .unwrap(),
1597 vec![
1598 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,
1599 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,
1600 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1601 ]
1602 );
1603 assert_eq!(
1604 bincode::serialize(&pin(Predicate::GeoRadius {
1605 field: "loc".into(),
1606 km: 400.0,
1607 }))
1608 .unwrap(),
1609 vec![
1610 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,
1611 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,
1612 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1613 ]
1614 );
1615 assert_eq!(
1616 bincode::serialize(&pin(Predicate::VectorSimilar {
1617 field: "emb".into(),
1618 min: 0.9,
1619 }))
1620 .unwrap(),
1621 vec![
1622 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,
1623 66, 6, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 101, 109, 98, 205, 204, 204, 204, 204, 204,
1624 236, 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1625 ]
1626 );
1627 }
1628
1629 #[test]
1630 fn any_variant_is_appended_at_discriminant_7() {
1631 let any_fe = pin(Predicate::Any(vec![Predicate::FieldEqual {
1647 field: "f".into(),
1648 }]));
1649 assert_eq!(
1650 bincode::serialize(&any_fe).unwrap(),
1651 vec![
1652 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,
1653 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,
1654 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0,
1655 ],
1656 "Any([FieldEqual{{f}}]) exact-bytes pin failed — discriminant or field layout changed"
1657 );
1658 let decoded: RuleDef = bincode::deserialize(&bincode::serialize(&any_fe).unwrap()).unwrap();
1660 assert_eq!(decoded, any_fe, "Any must round-trip via bincode");
1661 let vs = pin(Predicate::VectorSimilar {
1664 field: "emb".into(),
1665 min: 0.9,
1666 });
1667 let vs_bytes = bincode::serialize(&vs).unwrap();
1668 let vs_decoded: RuleDef = bincode::deserialize(&vs_bytes).unwrap();
1669 assert_eq!(
1670 vs_decoded.predicate,
1671 Predicate::VectorSimilar {
1672 field: "emb".into(),
1673 min: 0.9
1674 },
1675 "VectorSimilar record must still decode after via fields appended"
1676 );
1677 }
1678
1679 #[test]
1680 fn approximate_variant_has_pinned_encoding() {
1681 assert_eq!(
1685 bincode::serialize(&pin_approx(Predicate::VectorSimilar {
1686 field: "emb".into(),
1687 min: 0.9,
1688 }))
1689 .unwrap(),
1690 vec![
1691 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,
1692 66, 6, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 101, 109, 98, 205, 204, 204, 204, 204, 204,
1693 236, 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 1, 0, 0, 0
1694 ]
1695 );
1696 let exact = bincode::serialize(&pin(Predicate::VectorSimilar {
1699 field: "emb".into(),
1700 min: 0.9,
1701 }))
1702 .unwrap();
1703 let approx = bincode::serialize(&pin_approx(Predicate::VectorSimilar {
1704 field: "emb".into(),
1705 min: 0.9,
1706 }))
1707 .unwrap();
1708 assert_eq!(exact.len(), approx.len());
1709 let n = exact.len();
1710 assert_eq!(&exact[..n - 4], &approx[..n - 4]);
1712 assert_eq!(exact[n - 4], 0u8, "exact: approximate=false");
1714 assert_eq!(approx[n - 4], 1u8, "approx: approximate=true");
1715 assert_eq!(&exact[n - 3..], &[0u8, 0, 0]);
1717 assert_eq!(&approx[n - 3..], &[0u8, 0, 0]);
1718 }
1719
1720 fn base_legacy() -> LegacyRuleDefNoVia {
1725 LegacyRuleDefNoVia {
1726 name: "r".into(),
1727 src_label: "A".into(),
1728 dst_label: "B".into(),
1729 predicate: Predicate::FieldEqual {
1730 field: "ind".into(),
1731 },
1732 edge_type: "E".into(),
1733 weight_prop: None,
1734 max_edges: Some(10),
1735 approximate: false,
1736 }
1737 }
1738
1739 fn base_current() -> RuleDef {
1740 RuleDef {
1741 name: "r".into(),
1742 src_label: "A".into(),
1743 dst_label: "B".into(),
1744 predicate: Predicate::FieldEqual {
1745 field: "ind".into(),
1746 },
1747 edge_type: "E".into(),
1748 weight_prop: None,
1749 max_edges: Some(10),
1750 approximate: false,
1751 via_label: None,
1752 via_edge: None,
1753 via_dir: None,
1754 }
1755 }
1756
1757 #[test]
1760 fn decode_rule_def_legacy_roundtrip() {
1761 let legacy_bytes = bincode::serialize(&base_legacy()).unwrap();
1762 let got = decode_rule_def(&legacy_bytes).expect("legacy decode must succeed");
1763 assert_eq!(got.name, "r");
1764 assert_eq!(got.src_label, "A");
1765 assert_eq!(got.max_edges, Some(10));
1766 assert!(!got.approximate);
1767 assert!(got.via_label.is_none(), "via_label must default to None");
1768 assert!(got.via_edge.is_none(), "via_edge must default to None");
1769 assert!(got.via_dir.is_none(), "via_dir must default to None");
1770 }
1771
1772 #[test]
1775 fn decode_rule_def_current_shape_roundtrip() {
1776 let current = RuleDef {
1777 via_label: Some("Mid".into()),
1778 via_edge: Some("hop".into()),
1779 via_dir: Some(core_storage::Direction::Out),
1780 ..base_current()
1781 };
1782 let bytes = bincode::serialize(¤t).unwrap();
1783 let got = decode_rule_def(&bytes).expect("current-shape decode must succeed");
1784 assert_eq!(got, current);
1785 }
1786
1787 #[test]
1789 fn decode_rule_def_garbage_returns_err() {
1790 let garbage = b"\xde\xad\xbe\xef\x00\x00\x00\x00";
1791 let err = decode_rule_def(garbage).unwrap_err();
1792 assert!(
1793 err.contains("current-shape"),
1794 "error must name current-shape attempt: {err}"
1795 );
1796 assert!(
1797 err.contains("legacy-shape"),
1798 "error must name legacy-shape attempt: {err}"
1799 );
1800 }
1801
1802 #[test]
1808 fn decode_rule_def_current_none_via_not_misidentified_as_legacy() {
1809 let current = base_current(); let legacy = base_legacy();
1811 let current_bytes = bincode::serialize(¤t).unwrap();
1812 let legacy_bytes = bincode::serialize(&legacy).unwrap();
1813
1814 assert_ne!(
1816 current_bytes, legacy_bytes,
1817 "current and legacy encodings must not be byte-identical"
1818 );
1819 assert_eq!(
1820 current_bytes.len(),
1821 legacy_bytes.len() + 3,
1822 "current is exactly 3 bytes longer (three Option::None fields)"
1823 );
1824
1825 let from_current =
1827 decode_rule_def(¤t_bytes).expect("current-shape must decode via current path");
1828 let from_legacy =
1829 decode_rule_def(&legacy_bytes).expect("legacy bytes must decode via legacy path");
1830 assert_eq!(
1831 from_current, from_legacy,
1832 "both paths must produce the same RuleDef"
1833 );
1834 assert!(from_current.via_label.is_none());
1835 assert!(from_current.via_edge.is_none());
1836 assert!(from_current.via_dir.is_none());
1837 }
1838}