Skip to main content

krabka_client_streams/store/
window.rs

1//! Window store over the byte backend.
2//!
3//! The keys are composite `WindowKeySchema` keys, and the values are
4//! `ValueAndTimestamp` values. This is a second typed store beside
5//! `KeyValueBytesStore`.
6use std::{
7    any::Any,
8    sync::{Arc, Mutex},
9};
10
11use async_trait::async_trait;
12use bytes::Bytes;
13use krabka_units::prelude::*;
14use tokio::sync::Mutex as AsyncMutex;
15
16use crate::{
17    processor::{record::RecordContext, serde::Serde},
18    store::{
19        api::StateStore,
20        byte::{ByteKeyValueStore, InMemoryBytes},
21        cache::{named::NamedCache, window::CachingWindowStore},
22        window_schema::{key_bytes_of, store_key, unwrap_value, window_start_of, wrap_value},
23    },
24};
25
26/// The window store's backing.
27///
28/// The backing is either a plain boxed byte store or a record-cache wrapper over
29/// it. A caller opts into `Cached` with [`WindowBytesStore::enable_cache`].
30/// Uncached stores keep their behavior exactly. This mirrors `kv::Backing`.
31enum Backing {
32    Plain(Box<dyn ByteKeyValueStore>),
33    Cached(CachingWindowStore),
34}
35
36impl Backing {
37    /// Reads a single key.
38    ///
39    /// `Cached` reads the cache first. `Plain` reads directly.
40    async fn get(&self, key: &[u8]) -> Option<Bytes> {
41        match self {
42            Backing::Plain(b) => b.get(key).await,
43            Backing::Cached(c) => c.get(key).await,
44        }
45    }
46    async fn range(&self, lo: &[u8], hi: &[u8]) -> Vec<(Bytes, Bytes)> {
47        match self {
48            Backing::Plain(b) => b.range(lo, hi).await,
49            Backing::Cached(c) => c.range(lo, hi).await,
50        }
51    }
52    async fn scan_all(&self) -> Vec<(Bytes, Bytes)> {
53        match self {
54            Backing::Plain(b) => b.scan_all().await,
55            Backing::Cached(c) => c.scan_all().await,
56        }
57    }
58
59    /// Processing-path write.
60    ///
61    /// `Plain` does a direct put. `Cached` does a write-back put that carries the
62    /// record context, and defers the changelog to flush.
63    async fn put(&mut self, key: Bytes, value: Bytes, ctx: RecordContext) {
64        match self {
65            Backing::Plain(b) => b.put(key, value).await,
66            Backing::Cached(c) => c.put(key, value, ctx),
67        }
68    }
69
70    /// Restore-path write that goes below the cache.
71    ///
72    /// This method never stages a dirty entry.
73    async fn apply(&mut self, key: Bytes, value: Option<Bytes>) {
74        match (self, value) {
75            (Backing::Plain(b), Some(v)) => b.put(key, v).await,
76            (Backing::Plain(b), None) => {
77                b.delete(&key).await;
78            }
79            (Backing::Cached(c), Some(v)) => c.put_inner(key, v).await,
80            (Backing::Cached(c), None) => c.delete_inner(&key).await,
81        }
82    }
83
84    async fn clear(&mut self) {
85        match self {
86            Backing::Plain(b) => b.clear().await,
87            Backing::Cached(c) => c.clear().await,
88        }
89    }
90
91    /// Serializes every entry, cache overlay included, as the store's payload.
92    async fn snapshot(&self) -> Bytes {
93        crate::store::snapshot::encode_byte_entries(&self.scan_all().await)
94    }
95
96    /// Replaces every entry with the payload's. The decode runs before the
97    /// wipe, so a malformed payload leaves the store untouched.
98    async fn restore(&mut self, data: &[u8]) -> Result<(), crate::error::StreamsClientError> {
99        let entries = crate::store::snapshot::decode_byte_entries(data)?;
100        self.clear().await;
101        for (key, value) in entries {
102            self.apply(key, Some(value)).await;
103        }
104        Ok(())
105    }
106}
107
108/// Typed windowed store keyed by `(K, windowStart)`.
109///
110/// The store holds `V` and a record timestamp. `fetch_single` returns
111/// `(storedTs, V)`, so the aggregator can compute
112/// `newTs = max(recordTs, storedTs)`.
113#[async_trait]
114pub trait WindowStore<K: Send + Sync, V: Send>: StateStore {
115    async fn fetch_single(&self, key: &K, window_start: i64) -> Option<(i64, V)>;
116    async fn fetch(&self, key: &K, time_from: i64, time_to: i64) -> Vec<(i64, V)>;
117    /// Like `fetch`, but also returns each window's stored record timestamp.
118    ///
119    /// The result is `(windowStart, recordTs, value)`. The sliding-window
120    /// aggregator uses this method, because it needs `windowMaxRecordTimestamp`
121    /// to place the left and right windows.
122    async fn fetch_with_ts(&self, key: &K, time_from: i64, time_to: i64) -> Vec<(i64, i64, V)>;
123    /// Returns every window across ALL keys whose `windowStart` is in
124    /// `[start_from, start_to]`.
125    ///
126    /// The result is `(key, windowStart, recordTs, value)`. The emit-final
127    /// closed-window scan uses this method. The byte layout is key-prefixed, so
128    /// this is a filtered full scan that mirrors the JVM `fetchAll`.
129    async fn fetch_all_in_range(&self, start_from: i64, start_to: i64) -> Vec<(K, i64, i64, V)>;
130    async fn put(&mut self, key: K, window_start: i64, value: V, record_ts: i64);
131}
132
133pub struct WindowBytesStore<K, V> {
134    name: String,
135    changelog_topic: String,
136    backing: Backing,
137    key_serde: Box<dyn Serde<K>>,
138    value_serde: Box<dyn Serde<V>>,
139    changelog: Vec<(Bytes, Option<Bytes>)>,
140    logging: bool,
141    /// Window size. The windowed store-key bytes encode only the window START and
142    /// the seqnum, not the size or the end. So the store must know its size to
143    /// rebuild `end = start + window_size` for the downstream
144    /// [`Windowed`](crate::dsl::windows::Windowed) key it forwards on cache flush.
145    window_size: Time,
146    /// Set by [`StateStore::set_record_context`]. The store attaches it to the
147    /// next cached write, so it can forward the deduped `Change` with the right
148    /// context on flush. It is only meaningful when `backing` is `Cached`.
149    pending_ctx: Option<RecordContext>,
150}
151
152impl<K: 'static, V: 'static> WindowBytesStore<K, V> {
153    #[must_use]
154    pub(crate) fn new(
155        name: String,
156        backend: Box<dyn ByteKeyValueStore>,
157        key_serde: Box<dyn Serde<K>>,
158        value_serde: Box<dyn Serde<V>>,
159        changelog_topic: String,
160        window_size: Time,
161    ) -> Self {
162        Self {
163            name,
164            changelog_topic,
165            backing: Backing::Plain(backend),
166            key_serde,
167            value_serde,
168            changelog: Vec::new(),
169            logging: true,
170            window_size,
171            pending_ctx: None,
172        }
173    }
174
175    #[must_use]
176    pub fn in_memory(
177        name: String,
178        key_serde: Box<dyn Serde<K>>,
179        value_serde: Box<dyn Serde<V>>,
180        changelog_topic: String,
181        window_size: Time,
182    ) -> Self {
183        Self::new(
184            name,
185            Box::new(InMemoryBytes::default()),
186            key_serde,
187            value_serde,
188            changelog_topic,
189            window_size,
190        )
191    }
192
193    /// Wraps this store's backend in a record cache.
194    ///
195    /// This method moves the backend into a [`CachingWindowStore`]. The caller
196    /// supplies the [`NamedCache`] registered in the task's `ThreadCache`. A
197    /// re-wrap of an already-cached store does nothing.
198    pub(crate) fn enable_cache(&mut self, cache: Arc<Mutex<NamedCache>>) {
199        if !matches!(self.backing, Backing::Plain(_)) {
200            return; // already cached
201        }
202        let placeholder = Backing::Plain(Box::new(InMemoryBytes::default()));
203        let Backing::Plain(backend) = std::mem::replace(&mut self.backing, placeholder) else {
204            unreachable!("guarded by the matches! above")
205        };
206        let inner: Arc<AsyncMutex<Box<dyn ByteKeyValueStore>>> = Arc::new(AsyncMutex::new(backend));
207        self.backing = Backing::Cached(CachingWindowStore::with_name(
208            cache,
209            inner,
210            self.name.clone(),
211        ));
212    }
213
214    /// Returns `true` when a record cache wraps this store's backend.
215    #[must_use]
216    pub(crate) fn is_cached(&self) -> bool {
217        matches!(self.backing, Backing::Cached(_))
218    }
219
220    /// Returns the context to stamp on the next cached write.
221    ///
222    /// The context is the stashed
223    /// [`set_record_context`](StateStore::set_record_context) if there is one. If
224    /// not, it is a default rooted at the changelog topic.
225    fn write_ctx(&self) -> RecordContext {
226        self.pending_ctx.clone().unwrap_or(RecordContext {
227            topic: self.changelog_topic.clone(),
228            partition: 0,
229            offset: 0,
230            timestamp: 0,
231        })
232    }
233}
234
235#[async_trait]
236impl<K: Send + 'static, V: Send + 'static> StateStore for WindowBytesStore<K, V> {
237    fn name(&self) -> &str {
238        &self.name
239    }
240    async fn flush(&mut self) {}
241    fn close(&mut self) {}
242    fn as_any_mut(&mut self) -> &mut dyn Any {
243        self
244    }
245    fn changelog_topic(&self) -> &str {
246        &self.changelog_topic
247    }
248    fn take_changelog(&mut self) -> Vec<(Bytes, Option<Bytes>)> {
249        std::mem::take(&mut self.changelog)
250    }
251    async fn apply_changelog(&mut self, key: Bytes, value: Option<Bytes>) {
252        // Restore writes go BELOW the cache (straight to the inner store) so they
253        // don't stage dirty entries that would be re-logged on the next flush.
254        self.backing.apply(key, value).await;
255    }
256    fn set_logging(&mut self, on: bool) {
257        self.logging = on;
258    }
259    fn as_iq(&self) -> Option<&dyn crate::store::iq::IqQueryable> {
260        Some(self)
261    }
262    fn set_record_context(&mut self, ctx: RecordContext) {
263        self.pending_ctx = Some(ctx);
264    }
265    #[allow(private_interfaces)]
266    fn enable_cache_erased(&mut self, cache: Arc<Mutex<NamedCache>>) -> bool {
267        self.enable_cache(cache);
268        true
269    }
270    fn is_cached_erased(&self) -> bool {
271        self.is_cached()
272    }
273    #[allow(private_interfaces)]
274    async fn flush_cache_into(
275        &mut self,
276        buffer: &mut std::collections::VecDeque<(usize, crate::processor::erased::ErasedRecord)>,
277        children: &[usize],
278    ) {
279        use crate::{
280            dsl::{
281                processors::change::Change,
282                windows::{Window, Windowed},
283            },
284            processor::erased::ErasedRecord,
285        };
286        let Backing::Cached(cache) = &self.backing else {
287            return;
288        };
289        // Vec<(store_key_bytes, old_wrapped, new_wrapped, ctx)>. The values are the
290        // *wrapped* bytes (`recordTs ‖ serialized`) the inner store holds.
291        let drained = cache.flush_with_old().await;
292        for (sk, old_wrapped, new_wrapped, ctx) in drained {
293            // Changelog logs the raw windowed store-key + the new wrapped value,
294            // exactly as the uncached `put` path does.
295            if self.logging {
296                self.changelog.push((sk.clone(), new_wrapped.clone()));
297            }
298            // Decode the windowed store-key once: inner key bytes + window start.
299            let window_start = window_start_of(&sk);
300            let key_bytes = key_bytes_of(&sk);
301            for &child in children {
302                let key: K = self
303                    .key_serde
304                    .deserialize(&self.changelog_topic, key_bytes)
305                    .expect("flush_cache_into window key deserialize");
306                // Unwrap each wrapped value to its raw aggregate bytes, then
307                // deserialize. `None` (tombstone / absent) stays `None`.
308                let old: Option<V> = old_wrapped.as_ref().map(|w| {
309                    let (_ts, raw) = unwrap_value(w);
310                    self.value_serde
311                        .deserialize(&self.changelog_topic, raw)
312                        .expect("flush_cache_into window old value deserialize")
313                });
314                let new: Option<V> = new_wrapped.as_ref().map(|w| {
315                    let (_ts, raw) = unwrap_value(w);
316                    self.value_serde
317                        .deserialize(&self.changelog_topic, raw)
318                        .expect("flush_cache_into window new value deserialize")
319                });
320                let windowed = Windowed {
321                    key,
322                    window: Window {
323                        start: window_start,
324                        end: window_start + self.window_size.millis_i64(),
325                    },
326                };
327                let change = Change { old, new };
328                buffer.push_back((
329                    child,
330                    ErasedRecord::new(Some(Box::new(windowed)), Box::new(change), ctx.timestamp),
331                ));
332            }
333        }
334    }
335    async fn clear(&mut self) {
336        self.backing.clear().await;
337        self.changelog.clear();
338    }
339    async fn snapshot(&mut self) -> Bytes {
340        self.backing.snapshot().await
341    }
342    async fn restore_snapshot(
343        &mut self,
344        data: Bytes,
345    ) -> Result<(), crate::error::StreamsClientError> {
346        self.backing.restore(&data).await?;
347        // The task rewinds to the cut, so anything logged since is gone.
348        self.changelog.clear();
349        Ok(())
350    }
351}
352
353// `WindowBytesStore` holds only `Box<dyn Serde<_>>` + byte buffers, so it is
354// `Send + Sync` for any `K`/`V` — no `Sync` bound needed on the impl.
355#[async_trait::async_trait]
356impl<K: Send + 'static, V: Send + 'static> crate::store::iq::IqQueryable
357    for WindowBytesStore<K, V>
358{
359    fn kind(&self) -> crate::store::iq::StoreKind {
360        crate::store::iq::StoreKind::Window
361    }
362    async fn iq_window_fetch_single(&self, key: &[u8], window_start: i64) -> Option<bytes::Bytes> {
363        let sk = store_key(key, window_start, 0);
364        let wrapped = self.backing.get(&sk).await?;
365        let (_ts, raw) = unwrap_value(&wrapped);
366        Some(bytes::Bytes::copy_from_slice(raw))
367    }
368    async fn iq_window_fetch(
369        &self,
370        key: &[u8],
371        time_from: i64,
372        time_to: i64,
373    ) -> Vec<(i64, bytes::Bytes)> {
374        let lo = store_key(key, time_from, 0);
375        let hi = store_key(key, time_to.saturating_add(1), 0);
376        let mut out = Vec::new();
377        for (k, wrapped) in self.backing.range(&lo, &hi).await {
378            if key_bytes_of(&k) != key {
379                continue;
380            }
381            let (_ts, raw) = unwrap_value(&wrapped);
382            out.push((window_start_of(&k), bytes::Bytes::copy_from_slice(raw)));
383        }
384        out
385    }
386
387    async fn iq2_execute(
388        &self,
389        query: &crate::store::iq::Iq2Query,
390    ) -> Result<Box<dyn Any + Send>, crate::store::iq::Iq2Failure> {
391        use crate::store::iq::{Iq2Failure, Iq2Query};
392
393        // Serialize any key boxes to bytes up front (before any `.await`) so
394        // `query` is dropped before the async phase. `dyn Any` deref works for
395        // both `Send` and `Send+Sync` boxes.
396        let ser = |b: &dyn Any| -> Result<bytes::Bytes, Iq2Failure> {
397            let k = b.downcast_ref::<K>().ok_or(Iq2Failure::KeyTypeMismatch)?;
398            Ok(self.key_serde.serialize(&self.changelog_topic, k))
399        };
400
401        match query {
402            Iq2Query::WindowKey {
403                key,
404                from_ts,
405                to_ts,
406            } => {
407                let kb = ser(&**key)?;
408                // Window starts are non-negative and encoded big-endian *signed*,
409                // so a negative lower bound (e.g. the `i64::MIN` default of an
410                // unbounded `WindowKeyQuery`) would sort above `0x00..` and make
411                // the byte range empty. Clamp the lower start to 0.
412                let from = (*from_ts).max(0);
413                let to = *to_ts;
414
415                let lo = store_key(&kb, from, 0);
416                let hi = store_key(&kb, to.saturating_add(1), 0);
417                let mut out: Vec<(i64, V)> = Vec::new();
418                for (sk, wrapped) in self.backing.range(&lo, &hi).await {
419                    if key_bytes_of(&sk) != kb.as_ref() {
420                        continue;
421                    }
422                    let (_ts, raw) = unwrap_value(&wrapped);
423                    out.push((
424                        window_start_of(&sk),
425                        self.value_serde
426                            .deserialize(&self.changelog_topic, raw)
427                            .expect("iqv2 window value deserialize"),
428                    ));
429                }
430                Ok(Box::new(out))
431            }
432            Iq2Query::WindowRange {
433                lo,
434                hi,
435                from_ts,
436                to_ts,
437            } => {
438                let lo_b = match lo {
439                    Some(b) => Some(ser(&**b)?),
440                    None => None,
441                };
442                let hi_b = match hi {
443                    Some(b) => Some(ser(&**b)?),
444                    None => None,
445                };
446                let from = *from_ts;
447                let to = *to_ts;
448
449                let mut out: Vec<((K, i64), V)> = Vec::new();
450                for (sk, wrapped) in self.backing.scan_all().await {
451                    let ws = window_start_of(&sk);
452                    if ws < from || ws > to {
453                        continue;
454                    }
455                    let kbytes = key_bytes_of(&sk);
456                    if lo_b.as_ref().is_some_and(|l| kbytes < l.as_ref()) {
457                        continue;
458                    }
459                    if hi_b.as_ref().is_some_and(|h| kbytes > h.as_ref()) {
460                        continue;
461                    }
462                    let key = self
463                        .key_serde
464                        .deserialize(&self.changelog_topic, kbytes)
465                        .expect("iqv2 window range key deserialize");
466                    let (_ts, raw) = unwrap_value(&wrapped);
467                    let value = self
468                        .value_serde
469                        .deserialize(&self.changelog_topic, raw)
470                        .expect("iqv2 window range value deserialize");
471                    out.push(((key, ws), value));
472                }
473                Ok(Box::new(out))
474            }
475            _ => Err(Iq2Failure::UnknownQueryType),
476        }
477    }
478}
479
480#[async_trait]
481impl<K: Send + Sync + 'static, V: Send + 'static> WindowStore<K, V> for WindowBytesStore<K, V> {
482    async fn fetch_single(&self, key: &K, window_start: i64) -> Option<(i64, V)> {
483        let kb = self.key_serde.serialize(&self.changelog_topic, key);
484        let sk = store_key(&kb, window_start, 0);
485        let wrapped = self.backing.get(&sk).await?;
486        let (ts, raw) = unwrap_value(&wrapped);
487        Some((
488            ts,
489            self.value_serde
490                .deserialize(&self.changelog_topic, raw)
491                .expect("window value deserialize"),
492        ))
493    }
494
495    async fn fetch(&self, key: &K, time_from: i64, time_to: i64) -> Vec<(i64, V)> {
496        let kb = self.key_serde.serialize(&self.changelog_topic, key);
497        let lo = store_key(&kb, time_from, 0);
498        let hi = store_key(&kb, time_to.saturating_add(1), 0);
499        let mut out = Vec::new();
500        for (k, wrapped) in self.backing.range(&lo, &hi).await {
501            // guard prefix collisions: only return entries whose inner key matches
502            if key_bytes_of(&k) != kb.as_ref() {
503                continue;
504            }
505            let (_ts, raw) = unwrap_value(&wrapped);
506            out.push((
507                window_start_of(&k),
508                self.value_serde
509                    .deserialize(&self.changelog_topic, raw)
510                    .expect("window value deserialize"),
511            ));
512        }
513        out
514    }
515
516    async fn fetch_with_ts(&self, key: &K, time_from: i64, time_to: i64) -> Vec<(i64, i64, V)> {
517        let kb = self.key_serde.serialize(&self.changelog_topic, key);
518        let lo = store_key(&kb, time_from, 0);
519        let hi = store_key(&kb, time_to.saturating_add(1), 0);
520        let mut out = Vec::new();
521        for (k, wrapped) in self.backing.range(&lo, &hi).await {
522            if key_bytes_of(&k) != kb.as_ref() {
523                continue;
524            }
525            let (ts, raw) = unwrap_value(&wrapped);
526            out.push((
527                window_start_of(&k),
528                ts,
529                self.value_serde
530                    .deserialize(&self.changelog_topic, raw)
531                    .expect("window value deserialize"),
532            ));
533        }
534        out
535    }
536
537    async fn fetch_all_in_range(&self, start_from: i64, start_to: i64) -> Vec<(K, i64, i64, V)> {
538        let mut out = Vec::new();
539        for (k, wrapped) in self.backing.scan_all().await {
540            let ws = window_start_of(&k);
541            if ws < start_from || ws > start_to {
542                continue;
543            }
544            let key = self
545                .key_serde
546                .deserialize(&self.changelog_topic, key_bytes_of(&k))
547                .expect("window key deserialize");
548            let (ts, raw) = unwrap_value(&wrapped);
549            let value = self
550                .value_serde
551                .deserialize(&self.changelog_topic, raw)
552                .expect("window value deserialize");
553            out.push((key, ws, ts, value));
554        }
555        out
556    }
557
558    async fn put(&mut self, key: K, window_start: i64, value: V, record_ts: i64) {
559        let kb = self.key_serde.serialize(&self.changelog_topic, &key);
560        let sk = store_key(&kb, window_start, 0);
561        let raw = self.value_serde.serialize(&self.changelog_topic, &value);
562        let wrapped = wrap_value(record_ts, &raw);
563        match &self.backing {
564            // Plain: write through + buffer the changelog now (today's behavior).
565            Backing::Plain(_) => {
566                self.backing
567                    .put(sk.clone(), wrapped.clone(), self.write_ctx())
568                    .await;
569                if self.logging {
570                    self.changelog.push((sk, Some(wrapped)));
571                }
572            }
573            // Cached: write-back only. The changelog record is deferred to
574            // `flush_cache_into` (the changelog store sits below the cache).
575            Backing::Cached(_) => {
576                let ctx = self.write_ctx();
577                self.backing.put(sk, wrapped, ctx).await;
578            }
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::processor::serde::{I64Serde, StringSerde};
587
588    #[tokio::test]
589    async fn put_fetch_single_and_range() {
590        let mut s = WindowBytesStore::<String, i64>::in_memory(
591            "w".into(),
592            Box::new(StringSerde),
593            Box::new(I64Serde),
594            "app-w-changelog".into(),
595            millis(10),
596        );
597        s.put("k".to_string(), 0, 1, 5).await;
598        s.put("k".to_string(), 0, 2, 7).await;
599        s.put("k".to_string(), 10, 9, 11).await;
600        assert_eq!(s.fetch_single(&"k".to_string(), 0).await, Some((7, 2)));
601        assert_eq!(s.fetch_single(&"k".to_string(), 10).await, Some((11, 9)));
602        assert_eq!(s.fetch_single(&"k".to_string(), 99).await, None);
603        assert_eq!(
604            s.fetch(&"k".to_string(), 0, 10).await,
605            vec![(0, 2), (10, 9)]
606        );
607        assert_eq!(s.take_changelog().len(), 3);
608    }
609
610    #[tokio::test]
611    async fn fetch_with_ts_returns_window_start_and_record_ts() {
612        let mut s = WindowBytesStore::<String, i64>::in_memory(
613            "w".into(),
614            Box::new(StringSerde),
615            Box::new(I64Serde),
616            "app-w-changelog".into(),
617            millis(10),
618        );
619        s.put("k".into(), 0, 10, 5).await; // window start 0, value 10, recordTs 5
620        s.put("k".into(), 10, 20, 17).await; // window start 10, value 20, recordTs 17
621        let got = s.fetch_with_ts(&"k".to_string(), 0, 10).await;
622        assert_eq!(got, vec![(0, 5, 10), (10, 17, 20)]); // (windowStart, recordTs, value)
623    }
624
625    #[tokio::test]
626    async fn fetch_all_in_range_scans_across_keys() {
627        let mut s = WindowBytesStore::<String, i64>::in_memory(
628            "w".into(),
629            Box::new(StringSerde),
630            Box::new(I64Serde),
631            "app-w-changelog".into(),
632            millis(10),
633        );
634        // Two keys, three windows. windowStart ∈ {0, 0, 10}.
635        s.put("a".into(), 0, 1, 5).await;
636        s.put("b".into(), 0, 7, 6).await;
637        s.put("a".into(), 10, 9, 12).await;
638
639        // Range [0,0] returns both windowStart==0 entries (sort to make order-independent).
640        let mut got = s.fetch_all_in_range(0, 0).await;
641        got.sort();
642        assert_eq!(
643            got,
644            vec![("a".to_string(), 0, 5, 1), ("b".to_string(), 0, 6, 7)]
645        );
646
647        // Range [0,10] returns all three.
648        assert_eq!(s.fetch_all_in_range(0, 10).await.len(), 3);
649        // Range above everything returns nothing.
650        assert!(s.fetch_all_in_range(11, 100).await.is_empty());
651    }
652
653    #[tokio::test]
654    async fn iq2_window_key_and_range() {
655        use crate::store::iq::{Iq2Query, IqQueryable};
656        let mut s = WindowBytesStore::<String, i64>::in_memory(
657            "w".into(),
658            Box::new(StringSerde),
659            Box::new(I64Serde),
660            "w-changelog".into(),
661            secs(1),
662        );
663        s.put("a".into(), 0, 10, 5).await;
664        s.put("a".into(), 1000, 20, 1005).await;
665        s.put("b".into(), 0, 30, 6).await;
666        let q: &dyn IqQueryable = s.as_iq().unwrap();
667
668        // WindowKeyQuery: key "a", starts in [0,1000], ascending.
669        let wk = q
670            .iq2_execute(&Iq2Query::WindowKey {
671                key: Box::new("a".to_string()),
672                from_ts: 0,
673                to_ts: 1000,
674            })
675            .await
676            .unwrap();
677        assert_eq!(
678            *wk.downcast::<Vec<(i64, i64)>>().unwrap(),
679            vec![(0, 10), (1000, 20)]
680        );
681
682        // WindowRangeQuery: all keys, starts in [0,0] → a@0 and b@0, ascending by key.
683        let wr = q
684            .iq2_execute(&Iq2Query::WindowRange {
685                lo: None,
686                hi: None,
687                from_ts: 0,
688                to_ts: 0,
689            })
690            .await
691            .unwrap();
692        assert_eq!(
693            *wr.downcast::<Vec<((String, i64), i64)>>().unwrap(),
694            vec![(("a".to_string(), 0), 10), (("b".to_string(), 0), 30)]
695        );
696
697        // WindowRangeQuery: key range [b, b] only.
698        let wr_b = q
699            .iq2_execute(&Iq2Query::WindowRange {
700                lo: Some(Box::new("b".to_string())),
701                hi: Some(Box::new("b".to_string())),
702                from_ts: 0,
703                to_ts: 2000,
704            })
705            .await
706            .unwrap();
707        assert_eq!(
708            *wr_b.downcast::<Vec<((String, i64), i64)>>().unwrap(),
709            vec![(("b".to_string(), 0), 30)]
710        );
711    }
712
713    // Regression: `WindowKeyQuery::with_key(k)` with no explicit time range
714    // defaults to `from_ts = i64::MIN`. Window starts are encoded big-endian
715    // *signed*, so an unclamped `i64::MIN` lower bound (`0x80..`) sorts ABOVE
716    // every non-negative start (`0x00..`) under memcmp and the byte range comes
717    // back empty — the "all windows for this key" query must still return them.
718    #[tokio::test]
719    async fn iq2_window_key_default_bounds_returns_all_windows() {
720        use crate::store::iq::{Iq2Query, IqQueryable};
721        let mut s = WindowBytesStore::<String, i64>::in_memory(
722            "w".into(),
723            Box::new(StringSerde),
724            Box::new(I64Serde),
725            "w-changelog".into(),
726            secs(1),
727        );
728        s.put("a".into(), 0, 10, 5).await;
729        s.put("a".into(), 1000, 20, 1005).await;
730        let q: &dyn IqQueryable = s.as_iq().unwrap();
731        let wk = q
732            .iq2_execute(&Iq2Query::WindowKey {
733                key: Box::new("a".to_string()),
734                from_ts: i64::MIN,
735                to_ts: i64::MAX,
736            })
737            .await
738            .unwrap();
739        assert_eq!(
740            *wk.downcast::<Vec<(i64, i64)>>().unwrap(),
741            vec![(0, 10), (1000, 20)]
742        );
743    }
744
745    // ── Record-cache tests (mirror kv.rs) ───────────────────────────────────
746
747    fn cached_store(window_size: Time) -> WindowBytesStore<String, i64> {
748        let mut s = WindowBytesStore::<String, i64>::in_memory(
749            "w".into(),
750            Box::new(StringSerde),
751            Box::new(I64Serde),
752            "w-changelog".into(),
753            window_size,
754        );
755        s.enable_cache(Arc::new(Mutex::new(NamedCache::new("w".into()))));
756        s
757    }
758
759    fn ctx_at(ts: i64) -> RecordContext {
760        RecordContext {
761            topic: "t".into(),
762            partition: 0,
763            offset: 0,
764            timestamp: ts,
765        }
766    }
767
768    #[tokio::test]
769    async fn cached_window_store_reads_your_writes() {
770        let mut s = cached_store(millis(10));
771        s.set_record_context(ctx_at(0));
772        // Two cached puts to the SAME windowed key (window start 0).
773        s.put("a".into(), 0, 1, 5).await;
774        s.put("a".into(), 0, 2, 7).await;
775        // Cache-first read sees the latest staged write (value 2, recordTs 7).
776        assert_eq!(s.fetch_single(&"a".to_string(), 0).await, Some((7, 2)));
777        // No changelog buffered while writes only touch the cache.
778        assert!(s.take_changelog().is_empty());
779    }
780
781    #[tokio::test]
782    async fn flush_cache_into_emits_deduped_windowed_change() {
783        use crate::dsl::{
784            processors::change::Change,
785            windows::{Window, Windowed},
786        };
787        let mut s = cached_store(millis(10));
788        // Seed a committed value (flush it through + clear the changelog buffer).
789        s.set_record_context(ctx_at(0));
790        s.put("a".into(), 0, 1, 5).await;
791        let mut seed = std::collections::VecDeque::new();
792        s.flush_cache_into(&mut seed, &[0]).await;
793        let _ = s.take_changelog();
794
795        // Two staged writes for the same window key under context ts=7.
796        s.set_record_context(ctx_at(7));
797        s.put("a".into(), 0, 2, 11).await;
798        s.put("a".into(), 0, 3, 13).await;
799
800        let mut buffer = std::collections::VecDeque::new();
801        s.flush_cache_into(&mut buffer, &[7]).await;
802        // Exactly ONE deduped record for the single fake child index 7.
803        assert_eq!(buffer.len(), 1);
804        let (child, rec) = &buffer[0];
805        assert_eq!(*child, 7);
806        // Timestamp comes from the dirty entry's record context.
807        assert_eq!(rec.timestamp, 7);
808        // Key downcasts to Windowed<String> with start 0 / end = start + size.
809        let key = rec
810            .key
811            .as_ref()
812            .unwrap()
813            .downcast_ref::<Windowed<String>>()
814            .unwrap();
815        assert_eq!(key.key, "a");
816        assert_eq!(key.window, Window { start: 0, end: 10 }); // end = start + window_size
817        // Value downcasts to Change<i64> { old = committed (1), new = latest (3) }.
818        let change = rec.value.downcast_ref::<Change<i64>>().unwrap();
819        assert_eq!(change.old, Some(1));
820        assert_eq!(change.new, Some(3));
821
822        // Changelog buffered the RAW windowed store-key + the latest wrapped value.
823        let cl = s.take_changelog();
824        assert_eq!(cl.len(), 1);
825        assert_eq!(
826            cl[0].0,
827            store_key(
828                &StringSerde.serialize("w-changelog", &"a".to_string()),
829                0,
830                0
831            )
832        );
833        assert_eq!(
834            cl[0].1,
835            Some(wrap_value(13, &I64Serde.serialize("w-changelog", &3)))
836        );
837
838        // Inner store now holds the write-through value.
839        assert_eq!(s.fetch_single(&"a".to_string(), 0).await, Some((13, 3)));
840    }
841
842    #[tokio::test]
843    async fn cached_sliding_window_uses_true_size_not_retention_span_for_key_end() {
844        // Regression: a CACHED sliding aggregate/reduce builds its window store
845        // with a retention basis of `2 * timeDifferenceMs` but a true window size
846        // of `1 * timeDifferenceMs`. The flushed `Windowed` key end must be
847        // `start + timeDifferenceMs`, NOT `start + 2*timeDifferenceMs`. Before the
848        // fix the store was constructed with the retention span as its window size,
849        // so the end was doubled (this assertion would see `end == 20`).
850        use crate::dsl::{
851            processors::change::Change,
852            windows::{Window, Windowed},
853        };
854
855        // D = 10ms (the real sliding window size = time_difference); the retention
856        // basis a sliding aggregate passes is 2*D = 20ms. The store's window size
857        // is the TRUE size (D), which is what `add_window_store` forwards.
858        const D: Time = millis(10);
859        let mut s = cached_store(D);
860
861        // Stage a cached put for a window starting at ws = 0.
862        let ws = 0;
863        s.set_record_context(ctx_at(7));
864        s.put("a".into(), ws, 1, 7).await;
865
866        let mut buffer = std::collections::VecDeque::new();
867        s.flush_cache_into(&mut buffer, &[0]).await;
868        assert_eq!(buffer.len(), 1);
869        let (_child, rec) = &buffer[0];
870        let key = rec
871            .key
872            .as_ref()
873            .unwrap()
874            .downcast_ref::<Windowed<String>>()
875            .unwrap();
876        assert_eq!(key.key, "a");
877        // end == ws + D (10), NOT ws + 2*D (20).
878        assert_eq!(
879            key.window,
880            Window {
881                start: ws,
882                end: ws + D.millis_i64()
883            }
884        );
885        let change = rec.value.downcast_ref::<Change<i64>>().unwrap();
886        assert_eq!(change.new, Some(1));
887    }
888
889    /// Cached `fetch` and `fetch_with_ts` route through `Backing::Cached::range`.
890    /// The cache overlay serves reads of your own writes before any flush, and
891    /// the cache wins over a colliding inner window.
892    #[tokio::test]
893    async fn cached_window_store_fetch_overlays_cache() {
894        let mut s = cached_store(millis(10));
895        s.set_record_context(ctx_at(0));
896        // Stage three windows for "a": starts 0, 10, 20.
897        s.put("a".into(), 0, 1, 5).await;
898        s.put("a".into(), 10, 2, 15).await;
899        s.put("a".into(), 20, 3, 25).await;
900
901        // fetch over [0, 20] returns all three in window order (range overlay).
902        assert_eq!(
903            s.fetch(&"a".to_string(), 0, 20).await,
904            vec![(0, 1), (10, 2), (20, 3)]
905        );
906        // fetch_with_ts surfaces each window's stored record ts.
907        assert_eq!(
908            s.fetch_with_ts(&"a".to_string(), 0, 20).await,
909            vec![(0, 5, 1), (10, 15, 2), (20, 25, 3)]
910        );
911        // Narrowed range excludes window 20.
912        assert_eq!(
913            s.fetch(&"a".to_string(), 0, 10).await,
914            vec![(0, 1), (10, 2)]
915        );
916    }
917
918    /// Cached `fetch_all_in_range` routes through `Backing::Cached::scan_all`. It
919    /// overlays the cache across all keys.
920    #[tokio::test]
921    async fn cached_window_store_fetch_all_overlays_cache() {
922        let mut s = cached_store(millis(10));
923        s.set_record_context(ctx_at(0));
924        s.put("a".into(), 0, 1, 5).await;
925        s.put("b".into(), 50, 2, 55).await;
926
927        // [0, 10] excludes b@50.
928        assert_eq!(
929            s.fetch_all_in_range(0, 10).await,
930            vec![("a".to_string(), 0, 5, 1)]
931        );
932        // [0, 50] includes both, in windowed-key order (a@0 < b@50).
933        assert_eq!(
934            s.fetch_all_in_range(0, 50).await,
935            vec![("a".to_string(), 0, 5, 1), ("b".to_string(), 50, 55, 2),]
936        );
937    }
938
939    /// `apply_changelog` on a cached window store writes BELOW the cache, so it
940    /// stages no dirty entry and the cache flush is empty. A `None` deletes
941    /// through.
942    #[tokio::test]
943    async fn cached_window_store_apply_changelog_goes_below_cache() {
944        let mut s = cached_store(millis(10));
945        let sk = store_key(
946            &StringSerde.serialize("w-changelog", &"a".to_string()),
947            0,
948            0,
949        );
950        let wrapped = wrap_value(7, &I64Serde.serialize("w-changelog", &9));
951        s.apply_changelog(sk.clone(), Some(wrapped)).await;
952        assert_eq!(s.fetch_single(&"a".to_string(), 0).await, Some((7, 9)));
953        // Restored below the cache: nothing to forward, no changelog buffered.
954        let mut buffer = std::collections::VecDeque::new();
955        s.flush_cache_into(&mut buffer, &[0]).await;
956        assert!(buffer.is_empty());
957        assert!(s.take_changelog().is_empty());
958
959        // None apply deletes through the inner store.
960        s.apply_changelog(sk, None).await;
961        assert_eq!(s.fetch_single(&"a".to_string(), 0).await, None);
962    }
963
964    /// `clear` on a cached window store empties the cache layer and the inner
965    /// store and drops the buffered changelog.
966    #[tokio::test]
967    async fn cached_window_store_clear_empties_everything() {
968        let mut s = cached_store(millis(10));
969        s.set_record_context(ctx_at(0));
970        s.put("a".into(), 0, 1, 5).await;
971        StateStore::clear(&mut s).await;
972        assert_eq!(s.fetch_single(&"a".to_string(), 0).await, None);
973        assert!(s.fetch_all_in_range(i64::MIN, i64::MAX).await.is_empty());
974        let mut buffer = std::collections::VecDeque::new();
975        s.flush_cache_into(&mut buffer, &[0]).await;
976        assert!(buffer.is_empty());
977    }
978
979    /// `enable_cache` is idempotent. A re-wrap of an already-cached store does
980    /// nothing.
981    #[tokio::test]
982    async fn enable_cache_is_idempotent() {
983        let mut s = WindowBytesStore::<String, i64>::in_memory(
984            "w".into(),
985            Box::new(StringSerde),
986            Box::new(I64Serde),
987            "w-changelog".into(),
988            millis(10),
989        );
990        assert!(!s.is_cached());
991        s.enable_cache(Arc::new(Mutex::new(NamedCache::new("w".into()))));
992        assert!(s.is_cached());
993        s.enable_cache(Arc::new(Mutex::new(NamedCache::new("w".into()))));
994        assert!(s.is_cached());
995    }
996
997    #[tokio::test]
998    async fn plain_window_store_unchanged() {
999        // A non-cached store logs the changelog immediately; flush_cache_into is a
1000        // no-op (no cache).
1001        let mut s = WindowBytesStore::<String, i64>::in_memory(
1002            "w".into(),
1003            Box::new(StringSerde),
1004            Box::new(I64Serde),
1005            "w-changelog".into(),
1006            millis(10),
1007        );
1008        s.put("a".into(), 0, 1, 5).await;
1009        assert_eq!(s.fetch_single(&"a".to_string(), 0).await, Some((5, 1)));
1010        let cl = s.take_changelog();
1011        assert_eq!(cl.len(), 1);
1012        let mut buffer = std::collections::VecDeque::new();
1013        s.flush_cache_into(&mut buffer, &[0]).await;
1014        assert!(buffer.is_empty());
1015    }
1016
1017    /// Plain-store lifecycle. `set_logging(false)` suppresses the changelog,
1018    /// `apply_changelog` restores below the store with Some and then deletes with
1019    /// None, `flush` and `close` do nothing, and `StateStore::clear` wipes the
1020    /// state and the changelog.
1021    #[tokio::test]
1022    async fn plain_window_store_lifecycle_and_apply_changelog() {
1023        let mut s = WindowBytesStore::<String, i64>::in_memory(
1024            "w".into(),
1025            Box::new(StringSerde),
1026            Box::new(I64Serde),
1027            "w-changelog".into(),
1028            millis(10),
1029        );
1030        // Logging off → no changelog buffered.
1031        s.set_logging(false);
1032        s.put("a".into(), 0, 1, 5).await;
1033        assert!(s.take_changelog().is_empty());
1034
1035        // apply_changelog restores below the store without re-logging.
1036        let sk = store_key(
1037            &StringSerde.serialize("w-changelog", &"b".to_string()),
1038            0,
1039            0,
1040        );
1041        s.apply_changelog(
1042            sk.clone(),
1043            Some(wrap_value(7, &I64Serde.serialize("w-changelog", &9))),
1044        )
1045        .await;
1046        assert_eq!(s.fetch_single(&"b".to_string(), 0).await, Some((7, 9)));
1047        assert!(s.take_changelog().is_empty());
1048        // None apply deletes through.
1049        s.apply_changelog(sk, None).await;
1050        assert_eq!(s.fetch_single(&"b".to_string(), 0).await, None);
1051
1052        // flush/close are no-ops; clear wipes the remaining "a" window + changelog.
1053        s.flush().await;
1054        s.close();
1055        StateStore::clear(&mut s).await;
1056        assert_eq!(s.fetch_single(&"a".to_string(), 0).await, None);
1057        assert!(s.take_changelog().is_empty());
1058    }
1059}