1use std::cmp::Reverse;
4use std::collections::BinaryHeap;
5
6use arrow::record_batch::RecordBatch;
7
8use crate::cep::pattern::CompiledPattern;
9
10#[derive(Debug, Clone)]
21pub struct PartialMatch {
22 pub stage_index: usize,
23 pub captured_events: Vec<RecordBatch>,
24 pub start_time_ms: i64,
25 pub captured_event_count: usize,
30}
31
32#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
41pub struct CepKeyState {
42 #[serde(skip)]
43 pub partial: Option<PartialMatch>,
44 pub last_event_ms: i64,
48}
49
50#[derive(Debug, Clone)]
52pub struct SequentialPatternMatcher {
53 pattern: CompiledPattern,
54}
55
56impl SequentialPatternMatcher {
57 pub fn new(pattern: CompiledPattern) -> Self {
58 Self { pattern }
59 }
60
61 pub fn process_event(
62 &self,
63 state: &mut CepKeyState,
64 stage_name: &str,
65 batch: RecordBatch,
66 event_time_ms: i64,
67 ) -> Vec<Vec<RecordBatch>> {
68 state.last_event_ms = event_time_ms;
69
70 if let Some(ref partial) = state.partial
71 && event_time_ms.saturating_sub(partial.start_time_ms)
72 > i64::try_from(self.pattern.window_ms).unwrap_or(i64::MAX)
73 {
74 state.partial = None;
75 }
76
77 let stage_idx = self
78 .pattern
79 .stages
80 .iter()
81 .position(|s| s.name == stage_name);
82
83 let Some(stage_idx) = stage_idx else {
84 return Vec::new();
85 };
86
87 if state.partial.is_none() {
88 if stage_idx != 0 {
89 return Vec::new();
90 }
91 state.partial = Some(PartialMatch {
92 stage_index: 0,
93 captured_events: vec![batch],
94 start_time_ms: event_time_ms,
95 captured_event_count: 1,
96 });
97 if self.pattern.stages.len() == 1 {
98 return self.take_complete(state);
99 }
100 return Vec::new();
101 }
102
103 if let Some(ref mut partial) = state.partial {
104 let expected_next = partial.stage_index + 1;
105 if stage_idx != expected_next {
106 return Vec::new();
107 }
108 partial.captured_events.push(batch);
109 partial.captured_event_count = partial.captured_events.len();
110 partial.stage_index = stage_idx;
111
112 if partial.stage_index + 1 == self.pattern.stages.len() {
113 return self.take_complete(state);
114 }
115 }
116 Vec::new()
117 }
118
119 fn take_complete(&self, state: &mut CepKeyState) -> Vec<Vec<RecordBatch>> {
120 state
121 .partial
122 .take()
123 .map(|p| vec![p.captured_events])
124 .unwrap_or_default()
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct PartitionedCepMatcher<K>
131where
132 K: std::hash::Hash + Eq + Clone + Ord,
133{
134 pattern: CompiledPattern,
135 states: std::collections::HashMap<K, (SequentialPatternMatcher, CepKeyState)>,
136 max_partitions: usize,
137 eviction_heap: BinaryHeap<Reverse<(i64, K)>>,
141}
142
143impl<K> PartitionedCepMatcher<K>
144where
145 K: std::hash::Hash + Eq + Clone + Ord,
146{
147 pub fn new(pattern: CompiledPattern) -> Self {
148 Self {
149 pattern,
150 states: std::collections::HashMap::new(),
151 max_partitions: 1024,
152 eviction_heap: BinaryHeap::new(),
153 }
154 }
155
156 pub fn process_event(
157 &mut self,
158 key: K,
159 stage_name: &str,
160 batch: RecordBatch,
161 event_time_ms: i64,
162 ) -> Vec<Vec<RecordBatch>> {
163 let entry = self.states.entry(key.clone()).or_insert_with(|| {
164 (
165 SequentialPatternMatcher::new(self.pattern.clone()),
166 CepKeyState::default(),
167 )
168 });
169 let result = entry
170 .0
171 .process_event(&mut entry.1, stage_name, batch, event_time_ms);
172
173 self.eviction_heap.push(Reverse((event_time_ms, key)));
175
176 if self.states.len() > self.max_partitions {
177 self.evict_stalest();
178 }
179 result
180 }
181
182 fn evict_stalest(&mut self) {
186 while let Some(Reverse((ts, k))) = self.eviction_heap.pop() {
187 if let Some((_, state)) = self.states.get(&k)
188 && state.last_event_ms == ts
189 {
190 self.states.remove(&k);
191 return;
192 }
193 }
194 }
195
196 pub fn evict_keys_before(&mut self, cutoff_ms: i64) {
200 self.states
201 .retain(|_, (_, state)| state.last_event_ms >= cutoff_ms);
202 }
204
205 pub fn partition_count(&self) -> usize {
207 self.states.len()
208 }
209
210 pub fn partial_signature(&self, key: &K) -> Option<(usize, i64)> {
223 self.states
224 .get(key)
225 .and_then(|(_, state)| state.partial.as_ref())
226 .map(|partial| (partial.stage_index, partial.start_time_ms))
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::cep::pattern::Pattern;
234 use arrow::array::{Int32Array, RecordBatch};
235 use arrow::datatypes::{DataType, Field, Schema};
236 use std::sync::Arc;
237 use std::time::Duration;
238
239 fn schema() -> Arc<Schema> {
240 Arc::new(Schema::new(vec![
241 Field::new("event_type", DataType::Utf8, false),
242 Field::new("timestamp", DataType::Int64, false),
243 Field::new("value", DataType::Int32, false),
244 ]))
245 }
246
247 fn batch(v: i32) -> RecordBatch {
248 RecordBatch::try_new(
249 Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)])),
250 vec![Arc::new(Int32Array::from(vec![v]))],
251 )
252 .unwrap()
253 }
254
255 fn rich_batch(event_type: &str, timestamp: i64, value: i32) -> RecordBatch {
256 RecordBatch::try_new(
257 schema(),
258 vec![
259 Arc::new(arrow::array::StringArray::from(vec![event_type])),
260 Arc::new(arrow::array::Int64Array::from(vec![timestamp])),
261 Arc::new(Int32Array::from(vec![value])),
262 ],
263 )
264 .unwrap()
265 }
266
267 #[test]
268 fn two_stage_pattern_matches() {
269 let pattern = Pattern::begin("a")
270 .followed_by("b")
271 .within(Duration::from_secs(5))
272 .compile()
273 .unwrap();
274 let matcher = SequentialPatternMatcher::new(pattern);
275 let mut state = CepKeyState::default();
276 assert!(
277 matcher
278 .process_event(&mut state, "a", batch(1), 100)
279 .is_empty()
280 );
281 let done = matcher.process_event(&mut state, "b", batch(2), 200);
282 assert_eq!(done.len(), 1);
283 assert_eq!(done[0].len(), 2);
284 }
285
286 #[test]
287 fn expired_partial_discarded() {
288 let pattern = Pattern::begin("a")
289 .followed_by("b")
290 .within(Duration::from_millis(50))
291 .compile()
292 .unwrap();
293 let matcher = SequentialPatternMatcher::new(pattern);
294 let mut state = CepKeyState::default();
295 matcher.process_event(&mut state, "a", batch(1), 0);
296 assert!(
297 matcher
298 .process_event(&mut state, "b", batch(2), 100)
299 .is_empty()
300 );
301 }
302
303 #[test]
304 fn empty_pattern_compile_rejected() {
305 let result = Pattern::begin("a")
306 .compile()
307 .unwrap() ;
309 assert_eq!(result.stages.len(), 1);
310 }
311
312 #[test]
313 fn single_stage_match_completes_immediately() {
314 let pattern = Pattern::begin("only")
315 .within(Duration::from_secs(1))
316 .compile()
317 .unwrap();
318 let matcher = SequentialPatternMatcher::new(pattern);
319 let mut state = CepKeyState::default();
320 let done = matcher.process_event(&mut state, "only", batch(42), 100);
321 assert_eq!(
322 done.len(),
323 1,
324 "single-stage pattern must complete on first match"
325 );
326 assert_eq!(done[0].len(), 1);
327 assert!(
328 state.partial.is_none(),
329 "state must be cleared after completion"
330 );
331 }
332
333 #[test]
334 fn boundary_event_at_exact_window_limit() {
335 let pattern = Pattern::begin("a")
336 .followed_by("b")
337 .within(Duration::from_millis(100))
338 .compile()
339 .unwrap();
340 let matcher = SequentialPatternMatcher::new(pattern);
341 let mut state = CepKeyState::default();
342 matcher.process_event(&mut state, "a", batch(1), 0);
343 let done = matcher.process_event(&mut state, "b", batch(2), 100);
345 assert_eq!(done.len(), 1, "event at exact window boundary must match");
346 }
347
348 #[test]
349 fn boundary_event_one_ms_past_window() {
350 let pattern = Pattern::begin("a")
351 .followed_by("b")
352 .within(Duration::from_millis(100))
353 .compile()
354 .unwrap();
355 let matcher = SequentialPatternMatcher::new(pattern);
356 let mut state = CepKeyState::default();
357 matcher.process_event(&mut state, "a", batch(1), 0);
358 let done = matcher.process_event(&mut state, "b", batch(2), 101);
360 assert!(done.is_empty(), "event past window must be discarded");
361 }
362
363 #[test]
364 fn partitioned_matcher_independent_keys() {
365 let pattern = Pattern::begin("a")
366 .followed_by("b")
367 .within(Duration::from_secs(5))
368 .compile()
369 .unwrap();
370 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
371 assert!(pm.process_event("k1".into(), "a", batch(1), 100).is_empty());
373 assert!(
375 pm.process_event("k2".into(), "a", batch(10), 200)
376 .is_empty()
377 );
378 let done = pm.process_event("k1".into(), "b", batch(2), 300);
380 assert_eq!(done.len(), 1);
381 assert!(
383 pm.process_event("k2".into(), "a", batch(11), 400)
384 .is_empty()
385 );
386 }
387
388 #[test]
389 fn partitioned_matcher_independent_state() {
390 let pattern = Pattern::begin("x")
391 .followed_by("y")
392 .within(Duration::from_secs(5))
393 .compile()
394 .unwrap();
395 let mut pm = PartitionedCepMatcher::<i32>::new(pattern);
396 pm.process_event(1, "x", batch(1), 100);
397 pm.process_event(2, "x", batch(2), 200);
398 assert!(pm.states.contains_key(&1));
400 assert!(pm.states.contains_key(&2));
401 }
402
403 #[test]
406 fn wrong_stage_name_ignored() {
407 let pattern = Pattern::begin("a").followed_by("b").compile().unwrap();
408 let matcher = SequentialPatternMatcher::new(pattern);
409 let mut state = CepKeyState::default();
410 let result = matcher.process_event(&mut state, "c", batch(1), 100);
411 assert!(result.is_empty());
412 assert!(
413 state.partial.is_none(),
414 "no partial match should be started"
415 );
416 }
417
418 #[test]
419 fn partial_state_persisted_after_first_event() {
420 let pattern = Pattern::begin("a").followed_by("b").compile().unwrap();
421 let matcher = SequentialPatternMatcher::new(pattern);
422 let mut state = CepKeyState::default();
423 matcher.process_event(&mut state, "a", batch(1), 100);
424 assert!(
425 state.partial.is_some(),
426 "partial must exist after first stage"
427 );
428 let partial = state.partial.as_ref().unwrap();
429 assert_eq!(partial.stage_index, 0);
430 assert_eq!(partial.captured_events.len(), 1);
431 assert_eq!(partial.start_time_ms, 100);
432 }
433
434 #[test]
435 fn stage_ordering_enforced() {
436 let pattern = Pattern::begin("a")
437 .followed_by("b")
438 .followed_by("c")
439 .compile()
440 .unwrap();
441 let matcher = SequentialPatternMatcher::new(pattern);
442 let mut state = CepKeyState::default();
443 matcher.process_event(&mut state, "a", batch(1), 100);
444 let result = matcher.process_event(&mut state, "c", batch(3), 200);
446 assert!(result.is_empty());
447 assert!(
448 state.partial.is_some(),
449 "partial should still be waiting for stage b"
450 );
451 }
452
453 #[test]
454 fn three_stage_sequential_match() {
455 let pattern = Pattern::begin("a")
456 .followed_by("b")
457 .followed_by("c")
458 .compile()
459 .unwrap();
460 let matcher = SequentialPatternMatcher::new(pattern);
461 let mut state = CepKeyState::default();
462
463 assert!(
464 matcher
465 .process_event(&mut state, "a", batch(1), 100)
466 .is_empty()
467 );
468 assert!(
469 matcher
470 .process_event(&mut state, "b", batch(2), 200)
471 .is_empty()
472 );
473 let done = matcher.process_event(&mut state, "c", batch(3), 300);
474
475 assert_eq!(done.len(), 1);
476 assert_eq!(done[0].len(), 3);
477 assert_eq!(
478 done[0][0]
479 .column(0)
480 .as_any()
481 .downcast_ref::<arrow::array::Int32Array>()
482 .unwrap()
483 .value(0),
484 1
485 );
486 assert_eq!(
487 done[0][1]
488 .column(0)
489 .as_any()
490 .downcast_ref::<arrow::array::Int32Array>()
491 .unwrap()
492 .value(0),
493 2
494 );
495 assert_eq!(
496 done[0][2]
497 .column(0)
498 .as_any()
499 .downcast_ref::<arrow::array::Int32Array>()
500 .unwrap()
501 .value(0),
502 3
503 );
504 }
505
506 #[test]
507 fn multiple_matches_on_same_key() {
508 let pattern = Pattern::begin("a")
509 .followed_by("b")
510 .within(Duration::from_secs(5))
511 .compile()
512 .unwrap();
513 let matcher = SequentialPatternMatcher::new(pattern);
514 let mut state = CepKeyState::default();
515
516 matcher.process_event(&mut state, "a", batch(1), 100);
518 let done1 = matcher.process_event(&mut state, "b", batch(2), 200);
519 assert_eq!(done1.len(), 1);
520 assert!(state.partial.is_none(), "state cleared after first match");
521
522 matcher.process_event(&mut state, "a", batch(10), 300);
524 let done2 = matcher.process_event(&mut state, "b", batch(20), 400);
525 assert_eq!(done2.len(), 1);
526 }
527
528 #[test]
529 fn last_event_ms_updated() {
530 let pattern = Pattern::begin("a").followed_by("b").compile().unwrap();
531 let matcher = SequentialPatternMatcher::new(pattern);
532 let mut state = CepKeyState::default();
533 assert_eq!(state.last_event_ms, 0);
534 matcher.process_event(&mut state, "a", batch(1), 500);
535 assert_eq!(state.last_event_ms, 500);
536 matcher.process_event(&mut state, "b", batch(2), 600);
537 assert_eq!(state.last_event_ms, 600);
538 }
539
540 #[test]
541 fn wrong_stage_between_matches_does_not_corrupt_state() {
542 let pattern = Pattern::begin("a")
543 .followed_by("b")
544 .within(Duration::from_secs(5))
545 .compile()
546 .unwrap();
547 let matcher = SequentialPatternMatcher::new(pattern);
548 let mut state = CepKeyState::default();
549
550 matcher.process_event(&mut state, "a", batch(1), 100);
551 assert!(
553 matcher
554 .process_event(&mut state, "x", batch(99), 150)
555 .is_empty()
556 );
557 let done = matcher.process_event(&mut state, "b", batch(2), 200);
559 assert_eq!(done.len(), 1);
560 }
561
562 #[test]
563 fn out_of_order_stage_after_partial_resets_correctly() {
564 let pattern = Pattern::begin("a")
565 .followed_by("b")
566 .within(Duration::from_secs(5))
567 .compile()
568 .unwrap();
569 let matcher = SequentialPatternMatcher::new(pattern);
570 let mut state = CepKeyState::default();
571
572 matcher.process_event(&mut state, "a", batch(1), 100);
574 assert!(
576 matcher
577 .process_event(&mut state, "a", batch(10), 150)
578 .is_empty()
579 );
580 let done = matcher.process_event(&mut state, "b", batch(2), 200);
582 assert_eq!(done.len(), 1);
583 }
584
585 #[test]
586 fn rich_batch_sequential_match() {
587 let pattern = Pattern::begin("login")
588 .followed_by("query")
589 .within(Duration::from_secs(10))
590 .compile()
591 .unwrap();
592 let matcher = SequentialPatternMatcher::new(pattern);
593 let mut state = CepKeyState::default();
594
595 let b1 = rich_batch("login", 1000, 0);
596 let b2 = rich_batch("query", 2000, 42);
597
598 assert!(
599 matcher
600 .process_event(&mut state, "login", b1, 1000)
601 .is_empty()
602 );
603 let done = matcher.process_event(&mut state, "query", b2, 2000);
604
605 assert_eq!(done.len(), 1);
606 assert_eq!(done[0].len(), 2);
607 let col = done[0][0]
609 .column(0)
610 .as_any()
611 .downcast_ref::<arrow::array::StringArray>()
612 .unwrap();
613 assert_eq!(col.value(0), "login");
614 let col = done[0][1]
615 .column(0)
616 .as_any()
617 .downcast_ref::<arrow::array::StringArray>()
618 .unwrap();
619 assert_eq!(col.value(0), "query");
620 }
621
622 #[test]
623 fn default_window_is_60s() {
624 let pattern = Pattern::begin("a").compile().unwrap();
625 assert_eq!(pattern.window_ms, 60_000);
626 }
627
628 #[test]
629 fn no_partial_match_when_no_events_processed() {
630 let pattern = Pattern::begin("a").followed_by("b").compile().unwrap();
631 let matcher = SequentialPatternMatcher::new(pattern);
632 let mut state = CepKeyState::default();
633 let result = matcher.process_event(&mut state, "b", batch(2), 100);
635 assert!(result.is_empty());
636 assert!(state.partial.is_none());
637 }
638
639 #[test]
642 fn partitioned_wrong_stage_ignored_per_key() {
643 let pattern = Pattern::begin("a")
644 .followed_by("b")
645 .within(Duration::from_secs(5))
646 .compile()
647 .unwrap();
648 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
649 pm.process_event("k1".into(), "a", batch(1), 100);
650 assert!(
652 pm.process_event("k1".into(), "x", batch(99), 200)
653 .is_empty()
654 );
655 let done = pm.process_event("k1".into(), "b", batch(2), 300);
657 assert_eq!(done.len(), 1);
658 }
659
660 #[test]
661 fn partitioned_multiple_matches_per_key() {
662 let pattern = Pattern::begin("a")
663 .followed_by("b")
664 .within(Duration::from_secs(5))
665 .compile()
666 .unwrap();
667 let mut pm = PartitionedCepMatcher::<i32>::new(pattern);
668
669 pm.process_event(1, "a", batch(1), 100);
671 let done1 = pm.process_event(1, "b", batch(2), 200);
672 assert_eq!(done1.len(), 1);
673
674 pm.process_event(1, "a", batch(10), 300);
676 let done2 = pm.process_event(1, "b", batch(20), 400);
677 assert_eq!(done2.len(), 1);
678 }
679
680 #[test]
681 fn partitioned_three_stage_match() {
682 let pattern = Pattern::begin("a")
683 .followed_by("b")
684 .followed_by("c")
685 .within(Duration::from_secs(10))
686 .compile()
687 .unwrap();
688 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
689
690 assert!(pm.process_event("k1".into(), "a", batch(1), 100).is_empty());
691 assert!(pm.process_event("k1".into(), "b", batch(2), 200).is_empty());
692 let done = pm.process_event("k1".into(), "c", batch(3), 300);
693 assert_eq!(done.len(), 1);
694 assert_eq!(done[0].len(), 3);
695 }
696
697 #[test]
698 fn partitioned_independent_timeout_per_key() {
699 let pattern = Pattern::begin("a")
700 .followed_by("b")
701 .within(Duration::from_millis(50))
702 .compile()
703 .unwrap();
704 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
705
706 pm.process_event("k1".into(), "a", batch(1), 0);
708 pm.process_event("k2".into(), "a", batch(10), 1000);
710
711 assert!(pm.process_event("k1".into(), "b", batch(2), 60).is_empty());
713
714 let done = pm.process_event("k2".into(), "b", batch(20), 1040);
716 assert_eq!(done.len(), 1);
717 }
718
719 #[test]
720 fn partitioned_wrong_key_stage_not_cross_contaminated() {
721 let pattern = Pattern::begin("a")
722 .followed_by("b")
723 .within(Duration::from_secs(5))
724 .compile()
725 .unwrap();
726 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
727
728 pm.process_event("k1".into(), "a", batch(1), 100);
729 pm.process_event("k2".into(), "a", batch(2), 200);
730
731 let done = pm.process_event("k1".into(), "b", batch(3), 300);
733 assert_eq!(done.len(), 1);
734
735 assert!(pm.states.get("k2").unwrap().1.partial.is_some());
737 let done2 = pm.process_event("k2".into(), "b", batch(4), 400);
738 assert_eq!(done2.len(), 1);
739 }
740
741 #[test]
742 fn partitioned_rich_batch_preserves_data() {
743 let pattern = Pattern::begin("click")
744 .followed_by("purchase")
745 .within(Duration::from_secs(30))
746 .compile()
747 .unwrap();
748 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
749
750 let b1 = rich_batch("click", 1000, 0);
751 let b2 = rich_batch("purchase", 2000, 99);
752
753 assert!(
754 pm.process_event("user1".into(), "click", b1, 1000)
755 .is_empty()
756 );
757 let done = pm.process_event("user1".into(), "purchase", b2, 2000);
758
759 assert_eq!(done.len(), 1);
760 assert_eq!(done[0].len(), 2);
761 let val_col = done[0][1]
762 .column(2)
763 .as_any()
764 .downcast_ref::<Int32Array>()
765 .unwrap();
766 assert_eq!(val_col.value(0), 99);
767 }
768
769 #[test]
770 fn partitioned_new_key_auto_created() {
771 let pattern = Pattern::begin("a").followed_by("b").compile().unwrap();
772 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
773 assert!(pm.states.is_empty());
774
775 pm.process_event("new_key".into(), "a", batch(1), 100);
776 assert!(pm.states.contains_key("new_key"));
777 assert_eq!(pm.states.len(), 1);
778 }
779
780 #[test]
783 fn negative_event_timestamps() {
784 let pattern = Pattern::begin("a")
785 .followed_by("b")
786 .within(Duration::from_secs(5))
787 .compile()
788 .unwrap();
789 let matcher = SequentialPatternMatcher::new(pattern);
790 let mut state = CepKeyState::default();
791 matcher.process_event(&mut state, "a", batch(1), -1000);
792 let done = matcher.process_event(&mut state, "b", batch(2), -500);
793 assert_eq!(done.len(), 1);
794 }
795
796 #[test]
797 fn negative_timestamp_window_expired() {
798 let pattern = Pattern::begin("a")
799 .followed_by("b")
800 .within(Duration::from_millis(100))
801 .compile()
802 .unwrap();
803 let matcher = SequentialPatternMatcher::new(pattern);
804 let mut state = CepKeyState::default();
805 matcher.process_event(&mut state, "a", batch(1), -200);
806 let done = matcher.process_event(&mut state, "b", batch(2), -50);
808 assert!(
809 done.is_empty(),
810 "event past window with negative timestamps must be discarded"
811 );
812 }
813
814 #[test]
815 fn large_window_millis() {
816 let pattern = Pattern::begin("a")
817 .followed_by("b")
818 .within(Duration::from_millis(u64::MAX / 2))
819 .compile()
820 .unwrap();
821 assert!(pattern.window_ms > 0);
822 let matcher = SequentialPatternMatcher::new(pattern);
823 let mut state = CepKeyState::default();
824 matcher.process_event(&mut state, "a", batch(1), 0);
825 let done = matcher.process_event(&mut state, "b", batch(2), 1_000_000);
826 assert_eq!(done.len(), 1);
827 }
828
829 #[test]
830 fn multi_row_batch_preserves_all_rows() {
831 let pattern = Pattern::begin("a").followed_by("b").compile().unwrap();
832 let matcher = SequentialPatternMatcher::new(pattern);
833 let mut state = CepKeyState::default();
834
835 let multi_batch = RecordBatch::try_new(
836 Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)])),
837 vec![Arc::new(Int32Array::from(vec![10, 20, 30]))],
838 )
839 .unwrap();
840
841 matcher.process_event(&mut state, "a", multi_batch.clone(), 100);
842 let done = matcher.process_event(&mut state, "b", batch(2), 200);
843 assert_eq!(done.len(), 1);
844 assert_eq!(done[0].len(), 2);
845 let col = done[0][0]
847 .column(0)
848 .as_any()
849 .downcast_ref::<Int32Array>()
850 .unwrap();
851 assert_eq!(col.len(), 3);
852 assert_eq!(col.value(0), 10);
853 assert_eq!(col.value(1), 20);
854 assert_eq!(col.value(2), 30);
855 }
856
857 #[test]
858 fn first_event_at_zero_time() {
859 let pattern = Pattern::begin("a")
860 .followed_by("b")
861 .within(Duration::from_secs(1))
862 .compile()
863 .unwrap();
864 let matcher = SequentialPatternMatcher::new(pattern);
865 let mut state = CepKeyState::default();
866 matcher.process_event(&mut state, "a", batch(1), 0);
867 let done = matcher.process_event(&mut state, "b", batch(2), 0);
868 assert_eq!(done.len(), 1);
869 }
870
871 #[test]
872 fn exact_duplicate_stage_names_reset_partial() {
873 let pattern = Pattern::begin("a").followed_by("a").compile().unwrap();
876 let matcher = SequentialPatternMatcher::new(pattern);
877 let mut state = CepKeyState::default();
878 matcher.process_event(&mut state, "a", batch(1), 100);
879 let result = matcher.process_event(&mut state, "a", batch(2), 200);
881 assert!(result.is_empty());
882 }
883
884 #[test]
885 fn five_stage_pattern() {
886 let pattern = Pattern::begin("s1")
887 .followed_by("s2")
888 .followed_by("s3")
889 .followed_by("s4")
890 .followed_by("s5")
891 .compile()
892 .unwrap();
893 let matcher = SequentialPatternMatcher::new(pattern);
894 let mut state = CepKeyState::default();
895 assert!(
896 matcher
897 .process_event(&mut state, "s1", batch(1), 100)
898 .is_empty()
899 );
900 assert!(
901 matcher
902 .process_event(&mut state, "s2", batch(2), 200)
903 .is_empty()
904 );
905 assert!(
906 matcher
907 .process_event(&mut state, "s3", batch(3), 300)
908 .is_empty()
909 );
910 assert!(
911 matcher
912 .process_event(&mut state, "s4", batch(4), 400)
913 .is_empty()
914 );
915 let done = matcher.process_event(&mut state, "s5", batch(5), 500);
916 assert_eq!(done.len(), 1);
917 assert_eq!(done[0].len(), 5);
918 }
919
920 #[test]
921 fn partitioned_many_keys() {
922 let pattern = Pattern::begin("a")
923 .followed_by("b")
924 .within(Duration::from_secs(5))
925 .compile()
926 .unwrap();
927 let mut pm = PartitionedCepMatcher::<i32>::new(pattern);
928 for k in 0..100 {
929 pm.process_event(k, "a", batch(k), k as i64 * 100);
930 }
931 assert_eq!(pm.states.len(), 100);
932 let done = pm.process_event(50, "b", batch(50), 5000);
934 assert_eq!(done.len(), 1);
935 assert!(pm.states.get(&0).unwrap().1.partial.is_some());
937 assert!(pm.states.get(&99).unwrap().1.partial.is_some());
938 }
939
940 #[test]
941 fn partitioned_completed_key_can_restart() {
942 let pattern = Pattern::begin("a")
943 .followed_by("b")
944 .within(Duration::from_secs(5))
945 .compile()
946 .unwrap();
947 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
948 pm.process_event("k".into(), "a", batch(1), 100);
949 let done1 = pm.process_event("k".into(), "b", batch(2), 200);
950 assert_eq!(done1.len(), 1);
951 assert!(pm.states.get("k").unwrap().1.partial.is_none());
952 pm.process_event("k".into(), "a", batch(10), 300);
954 let done2 = pm.process_event("k".into(), "b", batch(20), 400);
955 assert_eq!(done2.len(), 1);
956 }
957
958 #[test]
959 fn cep_key_state_default_values() {
960 let state = CepKeyState::default();
961 assert!(state.partial.is_none());
962 assert_eq!(state.last_event_ms, 0);
963 }
964
965 #[test]
966 fn partial_match_default_values() {
967 let pm = PartialMatch {
968 stage_index: 0,
969 captured_events: Vec::new(),
970 start_time_ms: 0,
971 captured_event_count: 0,
972 };
973 assert_eq!(pm.stage_index, 0);
974 assert!(pm.captured_events.is_empty());
975 assert_eq!(pm.start_time_ms, 0);
976 }
977
978 #[test]
979 fn compiled_pattern_clone() {
980 let pattern = Pattern::begin("a")
981 .followed_by("b")
982 .within(Duration::from_secs(5))
983 .compile()
984 .unwrap();
985 let cloned = pattern.clone();
986 assert_eq!(cloned.stages.len(), pattern.stages.len());
987 assert_eq!(cloned.window_ms, pattern.window_ms);
988 }
989
990 #[test]
991 fn cep_key_state_serde_skips_partial_but_preserves_metadata() {
992 let state = CepKeyState {
995 last_event_ms: 1_234_567,
996 ..Default::default()
997 };
998 let json = serde_json::to_string(&state).unwrap();
999 let restored: CepKeyState = serde_json::from_str(&json).unwrap();
1000 assert_eq!(restored.last_event_ms, 1_234_567);
1001 assert!(restored.partial.is_none());
1002 }
1003
1004 #[test]
1005 fn sequential_matcher_clone() {
1006 let pattern = Pattern::begin("a").compile().unwrap();
1007 let matcher = SequentialPatternMatcher::new(pattern);
1008 let cloned = matcher.clone();
1009 let mut state = CepKeyState::default();
1010 let done = cloned.process_event(&mut state, "a", batch(1), 100);
1011 assert_eq!(done.len(), 1);
1012 }
1013
1014 #[test]
1015 fn partitioned_matcher_clone() {
1016 let pattern = Pattern::begin("a").followed_by("b").compile().unwrap();
1017 let mut pm = PartitionedCepMatcher::<String>::new(pattern);
1018 pm.process_event("k1".into(), "a", batch(1), 100);
1019 let cloned = pm.clone();
1020 assert!(cloned.states.contains_key("k1"));
1021 }
1022
1023 #[test]
1024 fn zero_duration_window_allows_same_time_match() {
1025 let pattern = Pattern::begin("a")
1026 .followed_by("b")
1027 .within(Duration::from_millis(0))
1028 .compile()
1029 .unwrap();
1030 let matcher = SequentialPatternMatcher::new(pattern);
1031 let mut state = CepKeyState::default();
1032 matcher.process_event(&mut state, "a", batch(1), 100);
1033 let done = matcher.process_event(&mut state, "b", batch(2), 100);
1035 assert_eq!(done.len(), 1);
1036 }
1037
1038 #[test]
1039 fn window_ms_default_is_60000() {
1040 let pattern = Pattern::begin("a").compile().unwrap();
1041 assert_eq!(pattern.window_ms, 60_000);
1042 }
1043
1044 #[test]
1045 fn pattern_stage_names_and_gap() {
1046 let pattern = Pattern::begin("start")
1047 .followed_by("end")
1048 .within(Duration::from_secs(10))
1049 .compile()
1050 .unwrap();
1051 assert_eq!(pattern.stages[0].name, "start");
1052 assert!(pattern.stages[0].max_gap_ms.is_none());
1053 assert_eq!(pattern.stages[1].name, "end");
1054 assert!(pattern.stages[1].max_gap_ms.is_none());
1055 }
1056}