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}