Skip to main content

krishiv_plan/cep/
matcher.rs

1//! Per-key sequential pattern matcher (R16 S2.2).
2
3use std::cmp::Reverse;
4use std::collections::BinaryHeap;
5
6use arrow::record_batch::RecordBatch;
7
8use crate::cep::pattern::CompiledPattern;
9
10/// Partial in-progress match.
11///
12/// The `captured_events` field is the live, in-memory list of record
13/// batches that have matched stages so far. The persistence-friendly
14/// companion fields (`stage_index`, `captured_event_count`,
15/// `start_time_ms`) can be serialised and snapshotted by the checkpoint
16/// coordinator; the executor is expected to keep `captured_events` in
17/// a separate durable store (or replay from the source on restart) so
18/// that the metadata in the checkpoint is sufficient to reconstruct
19/// the partial match.
20#[derive(Debug, Clone)]
21pub struct PartialMatch {
22    pub stage_index: usize,
23    pub captured_events: Vec<RecordBatch>,
24    pub start_time_ms: i64,
25    /// Number of events captured so far; mirrors `captured_events.len()`
26    /// so the field can be serialised by the checkpoint coordinator
27    /// (the actual `RecordBatch` payloads live in a separate durable
28    /// store keyed by this count). Kept in sync by `process_event`.
29    pub captured_event_count: usize,
30}
31
32/// Per-key CEP state.
33///
34/// `Serialize`/`Deserialize` (via `serde`) allow per-key state to be
35/// snapshotted by the checkpoint coordinator. The `partial` field is
36/// not serialised directly because `RecordBatch` does not implement
37/// `Serialize`; the metadata captured in the separate
38/// `captured_event_count` field on `PartialMatch` is enough to recover
39/// the partial state from a replay log on restart.
40#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
41pub struct CepKeyState {
42    #[serde(skip)]
43    pub partial: Option<PartialMatch>,
44    /// Wall-clock event time (ms) of the most recent event processed for this
45    /// key.  Updated on every `process_event` call; useful for idle-key
46    /// detection and TTL eviction by the streaming executor.
47    pub last_event_ms: i64,
48}
49
50/// Sequential pattern matcher for one key.
51#[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/// Partitioned wrapper routing events to per-key [`SequentialPatternMatcher`] instances (P3-27).
129#[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    /// Min-heap of `(last_event_ms, key)` for O(log n) stalest-key eviction.
138    /// Entries may be stale (key removed or timestamp updated); check against
139    /// `states` before evicting.
140    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        // Push the updated timestamp for this key onto the eviction heap.
174        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    /// Evict the stalest partition key in O(log n) using the eviction heap.
183    /// Skips heap entries that are stale (key no longer exists or its recorded
184    /// timestamp no longer matches the heap entry).
185    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    /// Remove all keys whose most recent event time is strictly before
197    /// `cutoff_ms`.  Called by the streaming CEP path after each batch to
198    /// bound memory for high-cardinality key spaces.
199    pub fn evict_keys_before(&mut self, cutoff_ms: i64) {
200        self.states
201            .retain(|_, (_, state)| state.last_event_ms >= cutoff_ms);
202        // Heap entries for evicted keys will be skipped lazily on next eviction.
203    }
204
205    /// Number of currently tracked partition keys.
206    pub fn partition_count(&self) -> usize {
207        self.states.len()
208    }
209
210    /// `(stage_index, start_time_ms)` of this key's live partial match, if any.
211    ///
212    /// Callers that do not know which pattern stage an incoming row represents
213    /// feed the row to each stage name in order, and must stop as soon as the
214    /// row has been consumed. Comparing this signature before and after
215    /// [`Self::process_event`] is how they detect consumption — it changes when
216    /// a partial starts, advances, or restarts after expiry (where
217    /// `stage_index` stays 0 but `start_time_ms` moves).
218    ///
219    /// Without that check the same row starts a partial at stage 0 and is then
220    /// advanced through every remaining stage, fabricating a complete match out
221    /// of a single event.
222    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() // 1-stage is fine
308            ;
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        // Exactly at the window boundary (0 + 100 = 100) — should still match.
344        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        // One ms past the window — must be discarded.
359        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        // Key "k1": start match
372        assert!(pm.process_event("k1".into(), "a", batch(1), 100).is_empty());
373        // Key "k2": start match
374        assert!(
375            pm.process_event("k2".into(), "a", batch(10), 200)
376                .is_empty()
377        );
378        // Key "k1": complete match
379        let done = pm.process_event("k1".into(), "b", batch(2), 300);
380        assert_eq!(done.len(), 1);
381        // Key "k2": still pending
382        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        // Both keys should have partial matches.
399        assert!(pm.states.contains_key(&1));
400        assert!(pm.states.contains_key(&2));
401    }
402
403    // ── SequentialPatternMatcher: untested paths ───────────────────────
404
405    #[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        // Skip "b", send "c" — should be ignored because stage_index expects b next.
445        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        // First match
517        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        // Second match on same key
523        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        // Wrong stage
552        assert!(
553            matcher
554                .process_event(&mut state, "x", batch(99), 150)
555                .is_empty()
556        );
557        // Correct stage still works
558        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        // Start with "a"
573        matcher.process_event(&mut state, "a", batch(1), 100);
574        // Send another "a" — stage_idx 0 != expected_next 1, so ignored
575        assert!(
576            matcher
577                .process_event(&mut state, "a", batch(10), 150)
578                .is_empty()
579        );
580        // "b" should still complete the match
581        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        // Verify event_type column is preserved
608        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        // Sending stage "b" with no prior state and stage_idx != 0 → ignored
634        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    // ── PartitionedCepMatcher: additional coverage ──────────────────────
640
641    #[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        // Wrong stage for k1
651        assert!(
652            pm.process_event("k1".into(), "x", batch(99), 200)
653                .is_empty()
654        );
655        // Correct stage for k1
656        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        // First match for key 1
670        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        // Second match for key 1
675        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        // k1 starts at t=0
707        pm.process_event("k1".into(), "a", batch(1), 0);
708        // k2 starts at t=1000
709        pm.process_event("k2".into(), "a", batch(10), 1000);
710
711        // k1 at t=60 → expired (60 > 50)
712        assert!(pm.process_event("k1".into(), "b", batch(2), 60).is_empty());
713
714        // k2 at t=1040 → still valid (1040 - 1000 = 40 <= 50)
715        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        // Complete k1
732        let done = pm.process_event("k1".into(), "b", batch(3), 300);
733        assert_eq!(done.len(), 1);
734
735        // k2 should still be at stage 0, not affected by k1 completion
736        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    // ── Additional deep-coverage tests ─────────────────────────────────
781
782    #[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        // -200 + 100 = -100, event at -50 is past window
807        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        // First captured batch should have 3 rows
846        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        // When stage names are duplicated, position() always finds stage 0,
874        // so the second "a" event is treated as starting a new match (not advancing).
875        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        // Second "a" hits stage_idx 0, but expected_next is 1, so it's ignored
880        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        // Complete only key 50
933        let done = pm.process_event(50, "b", batch(50), 5000);
934        assert_eq!(done.len(), 1);
935        // Other keys still partial
936        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        // Restart
953        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        // `partial` is skipped because `RecordBatch` doesn't impl Serialize;
993        // `last_event_ms` must survive the round trip.
994        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        // Exactly at boundary (100 - 100 = 0 <= 0)
1034        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}