1use std::collections::HashSet;
2use uni_store::storage::direction::Direction;
3
4pub type NfaStateId = u16;
6
7pub const DEFAULT_MAX_HOPS: usize = 128;
9
10#[derive(Clone, Debug, PartialEq)]
12pub enum PathMode {
13 Walk,
15 Trail,
17 Acyclic,
19 Simple,
21}
22
23#[derive(Clone, Debug)]
25pub enum PathSelector {
26 All,
28 Any,
30 AnyShortest,
32 AllShortest,
34 ShortestK(usize),
36}
37
38#[derive(Clone, Debug)]
40pub enum VlpOutputMode {
41 EndpointsOnly,
43 LengthOnly { needs_max: bool },
45 CountOnly,
47 FullPath,
49 StepVariable,
51 ShortestPath { selector: PathSelector },
53 Existential,
55}
56
57#[derive(Clone, Debug, PartialEq)]
62pub enum VertexConstraint {
63 Label(String),
65}
66
67#[derive(Clone, Debug)]
69pub struct QppStep {
70 pub edge_type_ids: Vec<u32>,
71 pub direction: Direction,
72 pub target_constraint: Option<VertexConstraint>,
73}
74
75#[derive(Clone, Debug)]
77pub struct NfaTransition {
78 pub from: NfaStateId,
79 pub to: NfaStateId,
80 pub edge_type_ids: Vec<u32>,
81 pub direction: Direction,
82}
83
84#[derive(Clone, Debug)]
101pub struct PathNfa {
102 transitions: Vec<NfaTransition>,
103 accepting_states: HashSet<NfaStateId>,
104 start_state: NfaStateId,
105 num_states: u16,
106 transitions_by_state: Vec<(usize, usize)>,
108 state_constraints: Vec<Option<VertexConstraint>>,
111}
112
113impl PathNfa {
114 pub fn from_vlp(
119 edge_type_ids: Vec<u32>,
120 direction: Direction,
121 min_hops: usize,
122 max_hops: usize,
123 ) -> Self {
124 if min_hops > max_hops {
127 return Self {
128 transitions: Vec::new(),
129 accepting_states: HashSet::new(),
130 start_state: 0,
131 num_states: 1,
132 transitions_by_state: vec![(0, 0)],
133 state_constraints: vec![None],
134 };
135 }
136
137 let num_states = (max_hops + 1) as u16;
138
139 let mut transitions = Vec::with_capacity(max_hops);
141 for i in 0..max_hops {
142 transitions.push(NfaTransition {
143 from: i as NfaStateId,
144 to: (i + 1) as NfaStateId,
145 edge_type_ids: edge_type_ids.clone(),
146 direction,
147 });
148 }
149
150 let accepting_states: HashSet<NfaStateId> =
152 (min_hops..=max_hops).map(|s| s as NfaStateId).collect();
153
154 let mut transitions_by_state = Vec::with_capacity(num_states as usize);
158 for i in 0..num_states {
159 if (i as usize) < max_hops {
160 transitions_by_state.push((i as usize, i as usize + 1));
161 } else {
162 let len = transitions.len();
163 transitions_by_state.push((len, len));
164 }
165 }
166
167 let state_constraints = vec![None; num_states as usize];
169
170 Self {
171 transitions,
172 accepting_states,
173 start_state: 0,
174 num_states,
175 transitions_by_state,
176 state_constraints,
177 }
178 }
179
180 pub fn transitions_from(&self, state: NfaStateId) -> &[NfaTransition] {
182 if (state as usize) < self.transitions_by_state.len() {
183 let (start, end) = self.transitions_by_state[state as usize];
184 &self.transitions[start..end]
185 } else {
186 &[]
187 }
188 }
189
190 pub fn is_accepting(&self, state: NfaStateId) -> bool {
192 self.accepting_states.contains(&state)
193 }
194
195 pub fn start_state(&self) -> NfaStateId {
197 self.start_state
198 }
199
200 pub fn num_states(&self) -> u16 {
202 self.num_states
203 }
204
205 pub fn accepting_states(&self) -> &HashSet<NfaStateId> {
207 &self.accepting_states
208 }
209
210 pub fn state_constraint(&self, state: NfaStateId) -> Option<&VertexConstraint> {
212 self.state_constraints
213 .get(state as usize)
214 .and_then(|c| c.as_ref())
215 }
216
217 pub fn from_qpp(steps: Vec<QppStep>, min_iterations: usize, max_iterations: usize) -> Self {
229 assert!(!steps.is_empty(), "QPP must have at least one step");
230
231 if min_iterations > max_iterations {
234 return Self {
235 transitions: Vec::new(),
236 accepting_states: HashSet::new(),
237 start_state: 0,
238 num_states: 1,
239 transitions_by_state: vec![(0, 0)],
240 state_constraints: vec![None],
241 };
242 }
243
244 let hops_per_iter = steps.len();
245 let total_hops = hops_per_iter * max_iterations;
246 let num_states = (total_hops + 1) as u16;
247
248 let mut transitions = Vec::with_capacity(total_hops);
250 let mut state_constraints = vec![None; num_states as usize];
251
252 for hop in 0..total_hops {
253 let step = &steps[hop % hops_per_iter];
254 let from = hop as NfaStateId;
255 let to = (hop + 1) as NfaStateId;
256
257 transitions.push(NfaTransition {
258 from,
259 to,
260 edge_type_ids: step.edge_type_ids.clone(),
261 direction: step.direction,
262 });
263
264 if let Some(ref constraint) = step.target_constraint {
266 state_constraints[to as usize] = Some(constraint.clone());
267 }
268 }
269
270 let mut accepting_states = HashSet::new();
272 for iter in min_iterations..=max_iterations {
273 accepting_states.insert((iter * hops_per_iter) as NfaStateId);
274 }
275
276 let mut transitions_by_state = Vec::with_capacity(num_states as usize);
278 for i in 0..num_states {
279 if (i as usize) < total_hops {
280 transitions_by_state.push((i as usize, i as usize + 1));
281 } else {
282 let len = transitions.len();
283 transitions_by_state.push((len, len));
284 }
285 }
286
287 Self {
288 transitions,
289 accepting_states,
290 start_state: 0,
291 num_states,
292 transitions_by_state,
293 state_constraints,
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn test_vlp_to_nfa_basic() {
304 let nfa = PathNfa::from_vlp(vec![1], Direction::Outgoing, 2, 5);
306 assert_eq!(nfa.num_states(), 6);
307 assert_eq!(nfa.start_state(), 0);
308 assert_eq!(nfa.transitions.len(), 5);
309 assert_eq!(
310 nfa.accepting_states(),
311 &[2, 3, 4, 5].into_iter().collect::<HashSet<NfaStateId>>()
312 );
313 for i in 0..6 {
315 assert!(nfa.state_constraint(i).is_none());
316 }
317 }
318
319 #[test]
320 fn test_vlp_to_nfa_unbounded() {
321 let nfa = PathNfa::from_vlp(vec![1], Direction::Outgoing, 1, DEFAULT_MAX_HOPS);
323 assert_eq!(nfa.num_states(), (DEFAULT_MAX_HOPS + 1) as u16);
324 assert!(!nfa.is_accepting(0));
325 assert!(nfa.is_accepting(1));
326 assert!(nfa.is_accepting(DEFAULT_MAX_HOPS as NfaStateId));
327 assert_eq!(nfa.transitions.len(), DEFAULT_MAX_HOPS);
328 }
329
330 #[test]
331 fn test_vlp_to_nfa_zero_min() {
332 let nfa = PathNfa::from_vlp(vec![1], Direction::Outgoing, 0, 3);
334 assert_eq!(nfa.num_states(), 4);
335 assert!(nfa.is_accepting(0));
336 assert!(nfa.is_accepting(1));
337 assert!(nfa.is_accepting(2));
338 assert!(nfa.is_accepting(3));
339 }
340
341 #[test]
342 fn test_vlp_to_nfa_exact() {
343 let nfa = PathNfa::from_vlp(vec![1], Direction::Outgoing, 3, 3);
345 assert_eq!(nfa.num_states(), 4);
346 assert!(!nfa.is_accepting(0));
347 assert!(!nfa.is_accepting(1));
348 assert!(!nfa.is_accepting(2));
349 assert!(nfa.is_accepting(3));
350 }
351
352 #[test]
353 fn test_vlp_to_nfa_multi_type() {
354 let nfa = PathNfa::from_vlp(vec![1, 2], Direction::Outgoing, 1, 3);
356 assert_eq!(nfa.num_states(), 4);
357 assert_eq!(nfa.transitions.len(), 3);
358 for t in &nfa.transitions {
359 assert_eq!(t.edge_type_ids, vec![1, 2]);
360 }
361 }
362
363 #[test]
364 fn test_vlp_to_nfa_direction_both() {
365 let nfa = PathNfa::from_vlp(vec![1], Direction::Both, 1, 2);
367 assert_eq!(nfa.num_states(), 3);
368 for t in &nfa.transitions {
369 assert_eq!(t.direction, Direction::Both);
370 }
371 }
372
373 #[test]
374 fn test_nfa_transitions_from() {
375 let nfa = PathNfa::from_vlp(vec![1], Direction::Outgoing, 1, 3);
376 let t0 = nfa.transitions_from(0);
378 assert_eq!(t0.len(), 1);
379 assert_eq!(t0[0].from, 0);
380 assert_eq!(t0[0].to, 1);
381
382 let t2 = nfa.transitions_from(2);
384 assert_eq!(t2.len(), 1);
385 assert_eq!(t2[0].from, 2);
386 assert_eq!(t2[0].to, 3);
387
388 let t3 = nfa.transitions_from(3);
390 assert_eq!(t3.len(), 0);
391
392 let t99 = nfa.transitions_from(99);
394 assert_eq!(t99.len(), 0);
395 }
396
397 #[test]
398 fn test_nfa_is_accepting() {
399 let nfa = PathNfa::from_vlp(vec![1], Direction::Outgoing, 2, 4);
400 assert!(!nfa.is_accepting(0));
401 assert!(!nfa.is_accepting(1));
402 assert!(nfa.is_accepting(2));
403 assert!(nfa.is_accepting(3));
404 assert!(nfa.is_accepting(4));
405 assert!(!nfa.is_accepting(5)); }
407
408 #[test]
411 fn test_qpp_two_hop_basic() {
412 let steps = vec![
416 QppStep {
417 edge_type_ids: vec![1],
418 direction: Direction::Outgoing,
419 target_constraint: None,
420 },
421 QppStep {
422 edge_type_ids: vec![2],
423 direction: Direction::Outgoing,
424 target_constraint: None,
425 },
426 ];
427 let nfa = PathNfa::from_qpp(steps, 1, 3);
428
429 assert_eq!(nfa.num_states(), 7);
430 assert_eq!(nfa.transitions.len(), 6);
431
432 assert!(!nfa.is_accepting(0));
434 assert!(!nfa.is_accepting(1));
435 assert!(nfa.is_accepting(2));
436 assert!(!nfa.is_accepting(3));
437 assert!(nfa.is_accepting(4));
438 assert!(!nfa.is_accepting(5));
439 assert!(nfa.is_accepting(6));
440 }
441
442 #[test]
443 fn test_qpp_state_constraints() {
444 let steps = vec![
448 QppStep {
449 edge_type_ids: vec![10],
450 direction: Direction::Outgoing,
451 target_constraint: Some(VertexConstraint::Label("Person".to_string())),
452 },
453 QppStep {
454 edge_type_ids: vec![20],
455 direction: Direction::Outgoing,
456 target_constraint: Some(VertexConstraint::Label("Company".to_string())),
457 },
458 ];
459 let nfa = PathNfa::from_qpp(steps, 1, 2);
460
461 assert_eq!(nfa.num_states(), 5); assert!(nfa.state_constraint(0).is_none());
465 assert_eq!(
466 nfa.state_constraint(1),
467 Some(&VertexConstraint::Label("Person".to_string()))
468 );
469 assert_eq!(
470 nfa.state_constraint(2),
471 Some(&VertexConstraint::Label("Company".to_string()))
472 );
473 assert_eq!(
474 nfa.state_constraint(3),
475 Some(&VertexConstraint::Label("Person".to_string()))
476 );
477 assert_eq!(
478 nfa.state_constraint(4),
479 Some(&VertexConstraint::Label("Company".to_string()))
480 );
481 }
482
483 #[test]
484 fn test_qpp_transitions_alternate() {
485 let steps = vec![
488 QppStep {
489 edge_type_ids: vec![1],
490 direction: Direction::Outgoing,
491 target_constraint: None,
492 },
493 QppStep {
494 edge_type_ids: vec![2],
495 direction: Direction::Incoming,
496 target_constraint: None,
497 },
498 ];
499 let nfa = PathNfa::from_qpp(steps, 1, 2);
500
501 assert_eq!(nfa.transitions.len(), 4);
502 assert_eq!(nfa.transitions[0].edge_type_ids, vec![1]);
503 assert_eq!(nfa.transitions[0].direction, Direction::Outgoing);
504 assert_eq!(nfa.transitions[1].edge_type_ids, vec![2]);
505 assert_eq!(nfa.transitions[1].direction, Direction::Incoming);
506 assert_eq!(nfa.transitions[2].edge_type_ids, vec![1]);
507 assert_eq!(nfa.transitions[2].direction, Direction::Outgoing);
508 assert_eq!(nfa.transitions[3].edge_type_ids, vec![2]);
509 assert_eq!(nfa.transitions[3].direction, Direction::Incoming);
510 }
511
512 #[test]
513 fn test_qpp_single_hop_equiv() {
514 let qpp_steps = vec![QppStep {
516 edge_type_ids: vec![1],
517 direction: Direction::Outgoing,
518 target_constraint: None,
519 }];
520 let qpp_nfa = PathNfa::from_qpp(qpp_steps, 2, 4);
521 let vlp_nfa = PathNfa::from_vlp(vec![1], Direction::Outgoing, 2, 4);
522
523 assert_eq!(qpp_nfa.num_states(), vlp_nfa.num_states());
524 assert_eq!(qpp_nfa.accepting_states(), vlp_nfa.accepting_states());
525 assert_eq!(qpp_nfa.transitions.len(), vlp_nfa.transitions.len());
526 }
527
528 #[test]
529 fn test_qpp_accepting_at_boundaries() {
530 let steps = vec![
533 QppStep {
534 edge_type_ids: vec![1],
535 direction: Direction::Outgoing,
536 target_constraint: None,
537 },
538 QppStep {
539 edge_type_ids: vec![2],
540 direction: Direction::Outgoing,
541 target_constraint: None,
542 },
543 QppStep {
544 edge_type_ids: vec![3],
545 direction: Direction::Outgoing,
546 target_constraint: None,
547 },
548 ];
549 let nfa = PathNfa::from_qpp(steps, 2, 3);
550
551 assert_eq!(nfa.num_states(), 10); for i in 0..10u16 {
555 if i == 6 || i == 9 {
556 assert!(nfa.is_accepting(i), "State {i} should be accepting");
557 } else {
558 assert!(!nfa.is_accepting(i), "State {i} should not be accepting");
559 }
560 }
561 }
562
563 #[test]
564 fn test_qpp_zero_min() {
565 let steps = vec![QppStep {
567 edge_type_ids: vec![1],
568 direction: Direction::Outgoing,
569 target_constraint: None,
570 }];
571 let nfa = PathNfa::from_qpp(steps, 0, 3);
572
573 assert!(nfa.is_accepting(0)); assert!(nfa.is_accepting(1)); assert!(nfa.is_accepting(2)); assert!(nfa.is_accepting(3)); }
578
579 #[test]
580 fn test_qpp_unbounded_capped() {
581 let steps = vec![
583 QppStep {
584 edge_type_ids: vec![1],
585 direction: Direction::Outgoing,
586 target_constraint: None,
587 },
588 QppStep {
589 edge_type_ids: vec![2],
590 direction: Direction::Outgoing,
591 target_constraint: None,
592 },
593 ];
594 let max_iter = DEFAULT_MAX_HOPS / steps.len();
595 let nfa = PathNfa::from_qpp(steps, 2, max_iter);
596
597 let expected_states = (max_iter * 2 + 1) as u16;
598 assert_eq!(nfa.num_states(), expected_states);
599 assert!(nfa.is_accepting((2 * 2) as NfaStateId)); assert!(nfa.is_accepting((max_iter * 2) as NfaStateId)); assert!(!nfa.is_accepting(1)); }
603
604 #[test]
605 fn test_vlp_empty_interval_no_panic() {
606 let nfa = PathNfa::from_vlp(vec![1], Direction::Outgoing, 3, 1);
609 assert!(
610 nfa.accepting_states().is_empty(),
611 "Empty interval NFA should have no accepting states"
612 );
613 assert_eq!(nfa.num_states(), 1);
615 assert_eq!(nfa.start_state(), 0);
616 }
617
618 #[test]
619 fn test_qpp_empty_interval_no_panic() {
620 let steps = vec![QppStep {
622 edge_type_ids: vec![1],
623 direction: Direction::Outgoing,
624 target_constraint: None,
625 }];
626 let nfa = PathNfa::from_qpp(steps, 3, 1);
627 assert!(
628 nfa.accepting_states().is_empty(),
629 "Empty interval QPP NFA should have no accepting states"
630 );
631 assert_eq!(nfa.num_states(), 1);
632 }
633
634 #[test]
635 fn test_vertex_constraint_label() {
636 let c = VertexConstraint::Label("Person".to_string());
637 assert_eq!(c, VertexConstraint::Label("Person".to_string()));
638 assert_ne!(c, VertexConstraint::Label("Company".to_string()));
639 }
640}