Skip to main content

objectiveai_cli/db/logs/
shadow.rs

1//! Shadow map for streaming-content rows.
2//!
3//! Keyed by [`OwnedRowKey`] (one variant per streaming-content
4//! table), valued by [`RowBody`] (the last-written body). On every
5//! incoming [`RowValue`]:
6//!
7//! 1. The borrowed [`RowKey`] is hashed and the shadow probed via
8//!    hashbrown's `raw_entry_mut` — no allocation in the Skip case.
9//! 2. If a row is found, [`RowValue::body_eq`] field-compares the
10//!    new body against the stored one. Fast bail on length / first-
11//!    byte mismatch, no fingerprint hash needed.
12//! 3. The verdict is returned as [`WriteOp::Insert`] / `Update` /
13//!    `Skip`. The writer dispatches the matching flat SQL — no
14//!    `ON CONFLICT` clauses, no upsert ambiguity.
15//!
16//! Allocations are confined to the Insert + Update paths: a single
17//! `to_owned_key()` (response_id String clone) on Insert, and a
18//! `to_body()` on either Insert or Update. The Skip path
19//! (overwhelmingly common in steady-state streaming) touches zero
20//! heap.
21
22use std::hash::{BuildHasher, Hash, Hasher};
23
24use hashbrown::HashMap;
25use hashbrown::hash_map::RawEntryMut;
26
27use super::row::{OwnedRowKey, RowBody, RowValue};
28
29/// What the writer should do with a particular row.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum WriteOp {
32    Insert,
33    Update,
34    Skip,
35}
36
37#[derive(Default)]
38pub struct Shadow {
39    rows: HashMap<OwnedRowKey, RowBody>,
40}
41
42impl Shadow {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Probe the shadow for `value`'s row and decide whether to write.
48    ///
49    /// Updates the shadow's stored body on Insert / Update. Returns
50    /// the verdict for the writer to dispatch.
51    pub fn record(&mut self, value: &RowValue<'_>) -> WriteOp {
52        let key_ref = value.key();
53
54        // Compute the hash from the borrowed key. Identical to the
55        // hash an owned key with the same fields would produce, since
56        // `&str` and `String` share the same Hash impl.
57        let hash = {
58            let mut state = self.rows.hasher().build_hasher();
59            key_ref.hash(&mut state);
60            state.finish()
61        };
62
63        let entry = self
64            .rows
65            .raw_entry_mut()
66            .from_hash(hash, |owned| key_ref.matches_owned(owned));
67
68        match entry {
69            RawEntryMut::Occupied(mut o) => {
70                if value.body_eq(o.get()) {
71                    WriteOp::Skip
72                } else {
73                    *o.get_mut() = value.to_body();
74                    WriteOp::Update
75                }
76            }
77            RawEntryMut::Vacant(v) => {
78                // We already computed `hash` via the table's hasher
79                // above; `insert_hashed_nocheck` stores the entry at
80                // that hash and lets the table rehash with its own
81                // BuildHasher on resize.
82                v.insert_hashed_nocheck(hash, key_ref.to_owned_key(), value.to_body());
83                WriteOp::Insert
84            }
85        }
86    }
87}