Skip to main content

krabka_client_streams/store/
kv.rs

1//! `KeyValueBytesStore<K,V>`, the single typed store that the registry holds and
2//! downcasts to.
3//!
4//! It adds the serde logic and the changelog-buffer logic over a pluggable
5//! `ByteKeyValueStore`.
6use std::{
7    any::Any,
8    sync::{Arc, Mutex},
9};
10
11use async_trait::async_trait;
12use bytes::Bytes;
13
14use crate::{
15    processor::{record::RecordContext, serde::Serde},
16    store::{
17        api::{KeyValueStore, StateStore},
18        byte::{ByteKeyValueStore, InMemoryBytes},
19        cache::{kv::CachingKeyValueStore, named::NamedCache},
20    },
21};
22
23/// The store's backing, which is either a plain boxed byte store or a
24/// record-cache wrapper over one.
25///
26/// [`KeyValueBytesStore::enable_cache`] opts into `Cached`. An uncached store
27/// keeps its behavior unchanged.
28enum Backing {
29    Plain(Box<dyn ByteKeyValueStore>),
30    // Constructed via `enable_cache`, which build-time cache wiring
31    // (`BuiltTopology::instantiate`) calls for materialized KV stores.
32    Cached(CachingKeyValueStore),
33}
34
35impl Backing {
36    /// Cache-first when the backing is `Cached`, which gives read-your-writes.
37    /// Otherwise the read goes direct.
38    async fn get(&self, key: &[u8]) -> Option<Bytes> {
39        match self {
40            Backing::Plain(b) => b.get(key).await,
41            Backing::Cached(c) => c.get(key).await,
42        }
43    }
44    async fn range(&self, lo: &[u8], hi: &[u8]) -> Vec<(Bytes, Bytes)> {
45        match self {
46            Backing::Plain(b) => b.range(lo, hi).await,
47            Backing::Cached(c) => c.range(lo, hi).await,
48        }
49    }
50    async fn scan_all(&self) -> Vec<(Bytes, Bytes)> {
51        match self {
52            Backing::Plain(b) => b.scan_all().await,
53            Backing::Cached(c) => c.scan_all().await,
54        }
55    }
56    async fn approx_len(&self) -> u64 {
57        match self {
58            Backing::Plain(b) => b.approx_len().await,
59            // No cheap merged count; the cache+inner overlay is materialized.
60            Backing::Cached(c) => c.scan_all().await.len() as u64,
61        }
62    }
63
64    /// Processing-path write. A plain backing does a direct put. A cached
65    /// backing does a write-back put that carries the record context, and it
66    /// defers the changelog to the flush.
67    async fn put(&mut self, key: Bytes, value: Bytes, ctx: RecordContext) {
68        match self {
69            Backing::Plain(b) => b.put(key, value).await,
70            Backing::Cached(c) => c.put(key, value, ctx).await,
71        }
72    }
73
74    /// Processing-path delete. A plain backing returns the previous value. A
75    /// cached backing stages a write-back tombstone, and it reads the previous
76    /// value cache-first for the typed return before it stages that
77    /// tombstone.
78    async fn delete(&mut self, key: Bytes, ctx: RecordContext) -> Option<Bytes> {
79        match self {
80            Backing::Plain(b) => b.delete(&key).await,
81            Backing::Cached(c) => {
82                let prev = c.get(&key).await;
83                c.delete(key, ctx).await;
84                prev
85            }
86        }
87    }
88
89    /// Restore-path write. It writes below the cache and never stages a dirty
90    /// entry.
91    async fn apply(&mut self, key: Bytes, value: Option<Bytes>) {
92        match (self, value) {
93            (Backing::Plain(b), Some(v)) => b.put(key, v).await,
94            (Backing::Plain(b), None) => {
95                b.delete(&key).await;
96            }
97            (Backing::Cached(c), Some(v)) => c.put_inner(key, v).await,
98            (Backing::Cached(c), None) => c.delete_inner(&key).await,
99        }
100    }
101
102    async fn clear(&mut self) {
103        match self {
104            Backing::Plain(b) => b.clear().await,
105            Backing::Cached(c) => c.clear().await,
106        }
107    }
108
109    /// Serializes every entry, cache overlay included, as the store's payload.
110    async fn snapshot(&self) -> Bytes {
111        crate::store::snapshot::encode_byte_entries(&self.scan_all().await)
112    }
113
114    /// Replaces every entry with the payload's. The decode runs before the
115    /// wipe, so a malformed payload leaves the store untouched.
116    async fn restore(&mut self, data: &[u8]) -> Result<(), crate::error::StreamsClientError> {
117        let entries = crate::store::snapshot::decode_byte_entries(data)?;
118        self.clear().await;
119        for (key, value) in entries {
120            self.apply(key, Some(value)).await;
121        }
122        Ok(())
123    }
124}
125
126pub struct KeyValueBytesStore<K, V> {
127    name: String,
128    changelog_topic: String,
129    backing: Backing,
130    key_serde: Box<dyn Serde<K>>,
131    value_serde: Box<dyn Serde<V>>,
132    changelog: Vec<(Bytes, Option<Bytes>)>,
133    logging: bool,
134    /// [`StateStore::set_record_context`] sets this field. The store attaches it
135    /// to the next cached write, so the flush can forward the deduped `Change`
136    /// with the right context. It matters only when `backing` is `Cached`.
137    pending_ctx: Option<RecordContext>,
138}
139
140impl<K: 'static, V: 'static> KeyValueBytesStore<K, V> {
141    #[must_use]
142    pub(crate) fn new(
143        name: String,
144        backend: Box<dyn ByteKeyValueStore>,
145        key_serde: Box<dyn Serde<K>>,
146        value_serde: Box<dyn Serde<V>>,
147        changelog_topic: String,
148    ) -> Self {
149        Self {
150            name,
151            changelog_topic,
152            backing: Backing::Plain(backend),
153            key_serde,
154            value_serde,
155            changelog: Vec::new(),
156            logging: true,
157            pending_ctx: None,
158        }
159    }
160
161    /// Wrap this store's backend in a record cache, which moves the backend into
162    /// a [`CachingKeyValueStore`].
163    ///
164    /// The caller supplies the [`NamedCache`] registered in the task's
165    /// `ThreadCache`. A second call on an already-cached store does nothing.
166    pub(crate) fn enable_cache(&mut self, cache: Arc<Mutex<NamedCache>>) {
167        if !matches!(self.backing, Backing::Plain(_)) {
168            return; // already cached
169        }
170        // Move the backend out, leaving a throwaway behind, then replace with the
171        // cache wrapper. Safe because we just confirmed `Plain`.
172        let placeholder = Backing::Plain(Box::new(InMemoryBytes::default()));
173        let Backing::Plain(backend) = std::mem::replace(&mut self.backing, placeholder) else {
174            unreachable!("guarded by the matches! above")
175        };
176        self.backing = Backing::Cached(CachingKeyValueStore::with_name(
177            cache,
178            backend,
179            self.name.clone(),
180        ));
181    }
182
183    /// Whether [`enable_cache`](Self::enable_cache) has wrapped this store's
184    /// backend in a record cache.
185    ///
186    /// The erased [`StateStore::is_cached_erased`] hook uses this method, so a
187    /// materializing processor can suppress its immediate forward. The
188    /// build-time wiring tests also use it.
189    #[must_use]
190    pub(crate) fn is_cached(&self) -> bool {
191        matches!(self.backing, Backing::Cached(_))
192    }
193
194    /// Convenience constructor for tests. It gives an in-memory-backed store.
195    #[must_use]
196    pub fn in_memory(
197        name: String,
198        key_serde: Box<dyn Serde<K>>,
199        value_serde: Box<dyn Serde<V>>,
200        changelog_topic: String,
201    ) -> Self {
202        Self::new(
203            name,
204            Box::new(InMemoryBytes::default()),
205            key_serde,
206            value_serde,
207            changelog_topic,
208        )
209    }
210
211    /// The context to stamp on the next cached write.
212    ///
213    /// It is the stashed
214    /// [`set_record_context`](StateStore::set_record_context) when one is
215    /// present, and otherwise a default rooted at the changelog topic.
216    /// Processing always sets the context first, so only a test that puts
217    /// without a context reaches the fallback.
218    fn write_ctx(&self) -> RecordContext {
219        self.pending_ctx.clone().unwrap_or(RecordContext {
220            topic: self.changelog_topic.clone(),
221            partition: 0,
222            offset: 0,
223            timestamp: 0,
224        })
225    }
226}
227
228#[async_trait]
229impl<K: Send + 'static, V: Send + 'static> StateStore for KeyValueBytesStore<K, V> {
230    fn name(&self) -> &str {
231        &self.name
232    }
233    async fn flush(&mut self) {}
234    fn close(&mut self) {}
235    fn as_any_mut(&mut self) -> &mut dyn Any {
236        self
237    }
238    fn changelog_topic(&self) -> &str {
239        &self.changelog_topic
240    }
241    fn take_changelog(&mut self) -> Vec<(Bytes, Option<Bytes>)> {
242        std::mem::take(&mut self.changelog)
243    }
244    async fn apply_changelog(&mut self, key: Bytes, value: Option<Bytes>) {
245        // Restore writes go BELOW the cache (straight to the inner store) so they
246        // don't stage dirty entries that would be re-logged on the next flush.
247        self.backing.apply(key, value).await;
248    }
249    fn set_logging(&mut self, on: bool) {
250        self.logging = on;
251    }
252    fn as_iq(&self) -> Option<&dyn crate::store::iq::IqQueryable> {
253        Some(self)
254    }
255    fn set_record_context(&mut self, ctx: RecordContext) {
256        self.pending_ctx = Some(ctx);
257    }
258    #[allow(private_interfaces)]
259    fn enable_cache_erased(&mut self, cache: Arc<Mutex<NamedCache>>) -> bool {
260        self.enable_cache(cache);
261        true
262    }
263    fn is_cached_erased(&self) -> bool {
264        self.is_cached()
265    }
266    #[allow(private_interfaces)]
267    async fn flush_cache_into(
268        &mut self,
269        buffer: &mut std::collections::VecDeque<(usize, crate::processor::erased::ErasedRecord)>,
270        children: &[usize],
271    ) {
272        use crate::{dsl::processors::change::Change, processor::erased::ErasedRecord};
273        let Backing::Cached(cache) = &self.backing else {
274            return;
275        };
276        let drained = cache.flush_with_old().await; // Vec<(kb, old_vb, new_vb, ctx)>
277        for (kb, old_vb, new_vb, ctx) in drained {
278            // Changelog is buffered at flush (the changelog store sits below the
279            // cache, so cached puts defer their changelog records until here).
280            if self.logging {
281                self.changelog.push((kb.clone(), new_vb.clone()));
282            }
283            // Re-build the typed key + `Change<V>` PER child from the cloneable
284            // byte buffers (mirrors `pipe`'s per-child re-deserialize and
285            // `forward`'s per-child clone): `Box<dyn Any>` / `Change<V>` for an
286            // unbounded `V` is not cloneable, so each child gets its own fresh
287            // deserialization. `children` is empty → the dirty entry is still
288            // written through + changelog-buffered above, just not forwarded.
289            for &child in children {
290                let k: K = self
291                    .key_serde
292                    .deserialize(&self.changelog_topic, &kb)
293                    .expect("flush_cache_into key deserialize");
294                let old: Option<V> = old_vb.as_ref().map(|b| {
295                    self.value_serde
296                        .deserialize(&self.changelog_topic, b)
297                        .expect("flush_cache_into old value deserialize")
298                });
299                let new: Option<V> = new_vb.as_ref().map(|b| {
300                    self.value_serde
301                        .deserialize(&self.changelog_topic, b)
302                        .expect("flush_cache_into new value deserialize")
303                });
304                let change = Change { old, new };
305                buffer.push_back((
306                    child,
307                    ErasedRecord::new(Some(Box::new(k)), Box::new(change), ctx.timestamp),
308                ));
309            }
310        }
311    }
312    async fn clear(&mut self) {
313        self.backing.clear().await;
314        self.changelog.clear();
315    }
316    async fn snapshot(&mut self) -> Bytes {
317        self.backing.snapshot().await
318    }
319    async fn restore_snapshot(
320        &mut self,
321        data: Bytes,
322    ) -> Result<(), crate::error::StreamsClientError> {
323        self.backing.restore(&data).await?;
324        // The task rewinds to the cut, so anything logged since is gone.
325        self.changelog.clear();
326        Ok(())
327    }
328}
329
330// The store struct holds only `Box<dyn Serde<_>>` + byte buffers (no bare `K`/`V`
331// fields), so it is `Send + Sync` for *any* `K`/`V`. `K: Send + V: Send` are
332// required so `Box<Option<V>>` / `Box<Vec<(K,V)>>` can be returned as
333// `Box<dyn Any + Send>` from `iq2_execute`.
334#[async_trait::async_trait]
335impl<K: Send + 'static, V: Send + 'static> crate::store::iq::IqQueryable
336    for KeyValueBytesStore<K, V>
337{
338    fn kind(&self) -> crate::store::iq::StoreKind {
339        crate::store::iq::StoreKind::KeyValue
340    }
341    async fn iq_kv_get(&self, key: &[u8]) -> Option<bytes::Bytes> {
342        self.backing.get(key).await
343    }
344    async fn iq_kv_range(&self, lo: &[u8], hi: &[u8]) -> Vec<(bytes::Bytes, bytes::Bytes)> {
345        // JVM `range` is inclusive `[lo, hi]`; the byte backend is half-open
346        // `[lo, hi)`. `hi ++ 0x00` is the least key strictly greater than `hi`,
347        // so `[lo, hi ++ 0x00)` == inclusive `[lo, hi]`.
348        let mut hi_succ = hi.to_vec();
349        hi_succ.push(0);
350        self.backing.range(lo, &hi_succ).await
351    }
352    async fn iq_kv_all(&self) -> Vec<(bytes::Bytes, bytes::Bytes)> {
353        self.backing.scan_all().await
354    }
355    async fn iq_kv_approx_count(&self) -> u64 {
356        self.backing.approx_len().await
357    }
358
359    async fn iq2_execute(
360        &self,
361        query: &crate::store::iq::Iq2Query,
362    ) -> Result<Box<dyn Any + Send>, crate::store::iq::Iq2Failure> {
363        use crate::store::iq::{Iq2Failure, Iq2Query};
364        let ser = |b: &Box<dyn Any + Send + Sync>| -> Result<bytes::Bytes, Iq2Failure> {
365            let k = b.downcast_ref::<K>().ok_or(Iq2Failure::KeyTypeMismatch)?;
366            Ok(self.key_serde.serialize(&self.changelog_topic, k))
367        };
368        match query {
369            Iq2Query::Key { key } => {
370                let kb = ser(key)?;
371                let out: Option<V> = self.backing.get(&kb).await.map(|vb| {
372                    self.value_serde
373                        .deserialize(&self.changelog_topic, &vb)
374                        .expect("iqv2 kv value deserialize")
375                });
376                Ok(Box::new(out))
377            }
378            Iq2Query::Range { lo, hi, descending } => {
379                let lo_b = match lo {
380                    Some(b) => Some(ser(b)?),
381                    None => None,
382                };
383                let hi_b = match hi {
384                    Some(b) => Some(ser(b)?),
385                    None => None,
386                };
387                let mut rows: Vec<(K, V)> = Vec::new();
388                for (kb, vb) in self.backing.scan_all().await {
389                    if lo_b.as_ref().is_some_and(|l| kb.as_ref() < l.as_ref()) {
390                        continue;
391                    }
392                    if hi_b.as_ref().is_some_and(|h| kb.as_ref() > h.as_ref()) {
393                        continue;
394                    }
395                    rows.push((
396                        self.key_serde
397                            .deserialize(&self.changelog_topic, &kb)
398                            .expect("iqv2 kv range key deserialize"),
399                        self.value_serde
400                            .deserialize(&self.changelog_topic, &vb)
401                            .expect("iqv2 kv range value deserialize"),
402                    ));
403                }
404                if *descending {
405                    rows.reverse();
406                }
407                Ok(Box::new(rows))
408            }
409            _ => Err(Iq2Failure::UnknownQueryType),
410        }
411    }
412}
413
414#[async_trait]
415impl<K: Send + Sync + 'static, V: Send + 'static> KeyValueStore<K, V> for KeyValueBytesStore<K, V> {
416    async fn get(&self, key: &K) -> Option<V> {
417        let kb = self.key_serde.serialize(&self.changelog_topic, key);
418        self.backing.get(&kb).await.map(|vb| {
419            self.value_serde
420                .deserialize(&self.changelog_topic, &vb)
421                .expect("store value deserialize")
422        })
423    }
424    async fn put(&mut self, key: K, value: V) {
425        let kb = self.key_serde.serialize(&self.changelog_topic, &key);
426        let vb = self.value_serde.serialize(&self.changelog_topic, &value);
427        match &self.backing {
428            // Plain: write through + buffer the changelog now (today's behavior).
429            Backing::Plain(_) => {
430                self.backing
431                    .put(kb.clone(), vb.clone(), self.write_ctx())
432                    .await;
433                if self.logging {
434                    self.changelog.push((kb, Some(vb)));
435                }
436            }
437            // Cached: write-back only. The changelog record is deferred to
438            // `flush_cache_changes` (the changelog store sits below the cache).
439            Backing::Cached(_) => {
440                let ctx = self.write_ctx();
441                self.backing.put(kb, vb, ctx).await;
442            }
443        }
444    }
445    async fn delete(&mut self, key: &K) -> Option<V> {
446        let kb = self.key_serde.serialize(&self.changelog_topic, key);
447        let ctx = self.write_ctx();
448        let cached = matches!(self.backing, Backing::Cached(_));
449        let prev = self.backing.delete(kb.clone(), ctx).await.map(|vb| {
450            self.value_serde
451                .deserialize(&self.changelog_topic, &vb)
452                .expect("store value deserialize")
453        });
454        // Plain logs the tombstone now; cached defers it to flush.
455        if self.logging && !cached {
456            self.changelog.push((kb, None));
457        }
458        prev
459    }
460    async fn range(&self, lo: &K, hi: &K) -> Vec<(K, V)> {
461        let lo_b = self.key_serde.serialize(&self.changelog_topic, lo);
462        let hi_b = self.key_serde.serialize(&self.changelog_topic, hi);
463        self.backing
464            .range(&lo_b, &hi_b)
465            .await
466            .into_iter()
467            .map(|(kb, vb)| {
468                (
469                    self.key_serde
470                        .deserialize(&self.changelog_topic, &kb)
471                        .expect("kv range key deserialize"),
472                    self.value_serde
473                        .deserialize(&self.changelog_topic, &vb)
474                        .expect("kv range value deserialize"),
475                )
476            })
477            .collect()
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use assert2::check;
484
485    use super::*;
486    use crate::processor::serde::{I64Serde, StringSerde};
487
488    fn store() -> KeyValueBytesStore<String, i64> {
489        KeyValueBytesStore::in_memory(
490            "s".into(),
491            Box::new(StringSerde),
492            Box::new(I64Serde),
493            "s-changelog".into(),
494        )
495    }
496
497    fn cached_store() -> KeyValueBytesStore<String, i64> {
498        let mut s = store();
499        s.enable_cache(Arc::new(Mutex::new(NamedCache::new("s".into()))));
500        s
501    }
502
503    fn ctx_at(ts: i64) -> RecordContext {
504        RecordContext {
505            topic: "t".into(),
506            partition: 0,
507            offset: 0,
508            timestamp: ts,
509        }
510    }
511
512    #[tokio::test]
513    async fn put_get_delete_and_changelog_buffer() {
514        let mut s = store();
515        s.put("a".into(), 1).await;
516        s.put("a".into(), 2).await;
517        check!(s.get(&"a".to_string()).await == Some(2));
518        check!(s.delete(&"a".to_string()).await == Some(2));
519        check!(s.get(&"a".to_string()).await == None);
520        let cl = s.take_changelog();
521        check!(cl.len() == 3);
522        check!(cl[2].1.is_none());
523        check!(s.take_changelog().is_empty());
524    }
525
526    #[tokio::test]
527    async fn range_returns_ordered_half_open() {
528        use bytes::Bytes;
529
530        use crate::processor::serde::BytesSerde;
531        let mut s = KeyValueBytesStore::<Bytes, Bytes>::in_memory(
532            "r".into(),
533            Box::new(BytesSerde),
534            Box::new(BytesSerde),
535            "r-cl".into(),
536        );
537        s.put(Bytes::from_static(&[1, 0]), Bytes::from_static(b"a"))
538            .await;
539        s.put(Bytes::from_static(&[1, 5]), Bytes::from_static(b"b"))
540            .await;
541        s.put(Bytes::from_static(&[2, 0]), Bytes::from_static(b"c"))
542            .await;
543        let r = s
544            .range(&Bytes::from_static(&[1, 0]), &Bytes::from_static(&[2, 0]))
545            .await; // [lo, hi)
546        assert_eq!(
547            r,
548            vec![
549                (Bytes::from_static(&[1, 0]), Bytes::from_static(b"a")),
550                (Bytes::from_static(&[1, 5]), Bytes::from_static(b"b")),
551            ]
552        );
553    }
554
555    #[tokio::test]
556    async fn iq2_key_and_range() {
557        use crate::store::iq::{Iq2Failure, Iq2Query, IqQueryable};
558        let mut s = store();
559        s.put("a".into(), 1).await;
560        s.put("b".into(), 2).await;
561        s.put("c".into(), 3).await;
562        let q: &dyn IqQueryable = s.as_iq().unwrap();
563
564        // KeyQuery hit / miss.
565        let got = q
566            .iq2_execute(&Iq2Query::Key {
567                key: Box::new("b".to_string()),
568            })
569            .await
570            .unwrap();
571        assert_eq!(*got.downcast::<Option<i64>>().unwrap(), Some(2));
572        let miss = q
573            .iq2_execute(&Iq2Query::Key {
574                key: Box::new("z".to_string()),
575            })
576            .await
577            .unwrap();
578        assert_eq!(*miss.downcast::<Option<i64>>().unwrap(), None);
579
580        // RangeQuery inclusive [a,b] ascending.
581        let r = q
582            .iq2_execute(&Iq2Query::Range {
583                lo: Some(Box::new("a".to_string())),
584                hi: Some(Box::new("b".to_string())),
585                descending: false,
586            })
587            .await
588            .unwrap();
589        assert_eq!(
590            *r.downcast::<Vec<(String, i64)>>().unwrap(),
591            vec![("a".to_string(), 1), ("b".to_string(), 2)]
592        );
593
594        // Unbounded both sides, descending → all, reversed.
595        let all_desc = q
596            .iq2_execute(&Iq2Query::Range {
597                lo: None,
598                hi: None,
599                descending: true,
600            })
601            .await
602            .unwrap();
603        assert_eq!(
604            *all_desc.downcast::<Vec<(String, i64)>>().unwrap(),
605            vec![
606                ("c".to_string(), 3),
607                ("b".to_string(), 2),
608                ("a".to_string(), 1)
609            ]
610        );
611
612        // Wrong key type → KeyTypeMismatch.
613        let bad = q
614            .iq2_execute(&Iq2Query::Key {
615                key: Box::new(7_i64),
616            })
617            .await;
618        assert_eq!(bad.err(), Some(Iq2Failure::KeyTypeMismatch));
619    }
620
621    #[tokio::test]
622    async fn apply_changelog_restores_without_re_logging() {
623        let mut s = store();
624        s.apply_changelog(
625            b"k".to_vec().into(),
626            Some(bytes::Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 7])),
627        )
628        .await;
629        check!(s.get(&"k".to_string()).await == Some(7));
630        check!(s.take_changelog().is_empty());
631        s.apply_changelog(b"k".to_vec().into(), None).await;
632        check!(s.get(&"k".to_string()).await == None);
633    }
634
635    #[tokio::test]
636    async fn cached_store_reads_your_writes() {
637        let mut s = cached_store();
638        s.set_record_context(ctx_at(0));
639        s.put("a".into(), 1).await;
640        s.put("a".into(), 2).await;
641        // Cache-first read sees the latest staged write.
642        check!(s.get(&"a".to_string()).await == Some(2));
643        // The inner backend has NOT been written yet (write-back deferred to flush).
644        let mut buffer = std::collections::VecDeque::new();
645        s.flush_cache_into(&mut buffer, &[0]).await;
646        check!(buffer.len() == 1);
647    }
648
649    #[tokio::test]
650    async fn cached_store_defers_changelog_until_flush() {
651        let mut s = cached_store();
652        s.set_record_context(ctx_at(0));
653        s.put("a".into(), 1).await;
654        s.put("a".into(), 2).await;
655        // No changelog buffered while writes only touch the cache.
656        check!(s.take_changelog().is_empty());
657        // Flushing the cache buffers exactly one deduped changelog record.
658        let mut buffer = std::collections::VecDeque::new();
659        s.flush_cache_into(&mut buffer, &[0]).await;
660        let cl = s.take_changelog();
661        check!(cl.len() == 1);
662        check!(cl[0].0 == StringSerde.serialize("s-changelog", &"a".to_string()));
663        check!(cl[0].1 == Some(I64Serde.serialize("s-changelog", &2)));
664    }
665
666    #[tokio::test]
667    async fn flush_cache_into_emits_deduped_change() {
668        use crate::dsl::processors::change::Change;
669        let mut s = cached_store();
670        // Seed committed a=1 (flush it through + clear the changelog buffer).
671        s.set_record_context(ctx_at(0));
672        s.put("a".into(), 1).await;
673        let mut seed = std::collections::VecDeque::new();
674        s.flush_cache_into(&mut seed, &[0]).await;
675        let _ = s.take_changelog();
676
677        // Two staged writes for the same key under context ts=7.
678        s.set_record_context(ctx_at(7));
679        s.put("a".into(), 2).await;
680        s.put("a".into(), 3).await;
681        // Flush into a single fake child index 7.
682        let mut buffer = std::collections::VecDeque::new();
683        s.flush_cache_into(&mut buffer, &[7]).await;
684        check!(buffer.len() == 1);
685        let (child, rec) = &buffer[0];
686        check!(*child == 7);
687        // Timestamp comes from the dirty entry's record context.
688        check!(rec.timestamp == 7);
689        let key = rec.key.as_ref().unwrap().downcast_ref::<String>().unwrap();
690        check!(key == "a");
691        let change = rec.value.downcast_ref::<Change<i64>>().unwrap();
692        // old = last-committed value (1); new = deduped latest (3).
693        check!(change.old == Some(1));
694        check!(change.new == Some(3));
695
696        // Inner store now holds the write-through value.
697        check!(s.get(&"a".to_string()).await == Some(3));
698    }
699
700    /// The cached routing for `range`, `scan_all`, `approx_len`, and the IQ
701    /// reads serves read-your-writes through the cache overlay before any
702    /// flush.
703    #[tokio::test]
704    async fn cached_store_range_scan_and_count_overlay() {
705        use crate::store::iq::IqQueryable;
706        let mut s = cached_store();
707        s.set_record_context(ctx_at(0));
708        s.put("a".into(), 1).await;
709        s.put("b".into(), 2).await;
710        s.put("c".into(), 3).await;
711
712        // Typed range routes through Backing::Cached::range (cache overlay).
713        let r = s.range(&"a".to_string(), &"c".to_string()).await; // [a, c)
714        check!(r == vec![("a".to_string(), 1), ("b".to_string(), 2)]);
715
716        // IQ all + approx-count route through Backing::Cached::scan_all.
717        check!(s.iq_kv_all().await.len() == 3);
718        check!(s.iq_kv_approx_count().await == 3);
719        // IQ get routes cache-first.
720        check!(s.iq_kv_get(b"b").await == Some(I64Serde.serialize("s-changelog", &2)));
721    }
722
723    /// On a cached store, a typed `delete` returns the earlier cached value. It
724    /// reads cache-first before it stages the tombstone.
725    #[tokio::test]
726    async fn cached_store_delete_returns_prev_and_stages_tombstone() {
727        let mut s = cached_store();
728        s.set_record_context(ctx_at(0));
729        s.put("a".into(), 5).await;
730
731        // delete sees the staged value as its return, without touching the
732        // changelog (cached defers it to flush).
733        check!(s.delete(&"a".to_string()).await == Some(5));
734        check!(s.take_changelog().is_empty());
735        check!(s.get(&"a".to_string()).await == None); // tombstone hides it
736
737        // Flushing emits the deduped tombstone Change + changelog record.
738        let mut buffer = std::collections::VecDeque::new();
739        s.flush_cache_into(&mut buffer, &[0]).await;
740        let cl = s.take_changelog();
741        check!(cl.len() == 1);
742        check!(cl[0].1.is_none());
743    }
744
745    /// `apply_changelog` on a cached store writes BELOW the cache. It stages no
746    /// dirty entry, so the next cache flush forwards nothing. A `None` value
747    /// deletes through the inner store.
748    #[tokio::test]
749    async fn cached_store_apply_changelog_goes_below_cache() {
750        let mut s = cached_store();
751        s.apply_changelog(
752            b"k".to_vec().into(),
753            Some(bytes::Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 7])),
754        )
755        .await;
756        check!(s.get(&"k".to_string()).await == Some(7));
757        // Restored below the cache: no dirty entry, so the cache flush is empty.
758        let mut buffer = std::collections::VecDeque::new();
759        s.flush_cache_into(&mut buffer, &[0]).await;
760        check!(buffer.is_empty());
761        check!(s.take_changelog().is_empty());
762
763        // A None apply deletes through the inner store.
764        s.apply_changelog(b"k".to_vec().into(), None).await;
765        check!(s.get(&"k".to_string()).await == None);
766    }
767
768    /// `clear` on a cached store empties both the cache layer and the inner store
769    /// and drops the buffered changelog.
770    #[tokio::test]
771    async fn cached_store_clear_empties_everything() {
772        use crate::store::iq::IqQueryable;
773        let mut s = cached_store();
774        s.set_record_context(ctx_at(0));
775        s.put("a".into(), 1).await;
776        StateStore::clear(&mut s).await;
777        check!(s.get(&"a".to_string()).await == None);
778        check!(s.iq_kv_all().await.is_empty());
779        let mut buffer = std::collections::VecDeque::new();
780        s.flush_cache_into(&mut buffer, &[0]).await;
781        check!(buffer.is_empty());
782    }
783
784    /// `enable_cache` is idempotent. A second call on an already-cached store
785    /// does nothing, because the guard returns early.
786    #[tokio::test]
787    async fn enable_cache_is_idempotent() {
788        let mut s = store();
789        check!(!s.is_cached());
790        s.enable_cache(Arc::new(Mutex::new(NamedCache::new("s".into()))));
791        check!(s.is_cached());
792        // Second call hits the early-return guard; still cached, no panic.
793        s.enable_cache(Arc::new(Mutex::new(NamedCache::new("s".into()))));
794        check!(s.is_cached());
795        check!(s.is_cached_erased());
796    }
797
798    #[tokio::test]
799    async fn plain_store_unchanged() {
800        // A non-cached store logs the changelog immediately and reads from the
801        // backend (no flush_cache_into needed).
802        let mut s = store();
803        s.put("a".into(), 1).await;
804        check!(s.get(&"a".to_string()).await == Some(1));
805        let cl = s.take_changelog();
806        check!(cl.len() == 1);
807        check!(cl[0].1 == Some(I64Serde.serialize("s-changelog", &1)));
808        // No cache → flush_cache_into forwards nothing.
809        let mut buffer = std::collections::VecDeque::new();
810        s.flush_cache_into(&mut buffer, &[0]).await;
811        check!(buffer.is_empty());
812        // flush / close are no-ops (in-memory durability is the changelog) and
813        // must not disturb the stored value.
814        s.flush().await;
815        s.close();
816        check!(s.get(&"a".to_string()).await == Some(1));
817    }
818}