rindle/storage.rs
1//! Spec `10` — **operator scratch state**: the small, sorted, string-keyed
2//! side-table a *stateful* operator (`Take`, `Cap`) keeps bookkeeping in across
3//! `fetch`/`push` calls. Ports `zql/src/ivm/operator.ts:132` `Storage` and
4//! `zql/src/ivm/memory-storage.ts`.
5//!
6//! It is **not** a row store, **not** the source index ([`crate::btree`]), and
7//! **not** the view tree. It is the IVM analogue of a per-operator hash-map,
8//! hoisted behind a trait so the server can later spill it to SQLite while the
9//! client keeps it in RAM (spec `10` §1.1).
10//!
11//! ## What's here vs. the SQLite backend
12//!
13//! This module is the backend-agnostic core: the [`Storage`] trait, the tight
14//! [`StorageValue`] enum, [`MemoryStorage`] for the client/test path, the
15//! [`StorageProvider`] seam, and the [`StorageFactory`] the builder calls. The
16//! server spill-to-SQLite backend (`DatabaseStorage`/`OpStorage` over rusqlite, spec
17//! `10` §4.4) lives in the `rindle-sqlite` crate, plugged in via
18//! `StorageFactory::custom(Rc::new(database_storage))` — so the core crate carries no
19//! `rusqlite` dependency.
20//!
21//! ## Backing structure — `BTreeMap` for now (a measured decision)
22//!
23//! [`MemoryStorage`] is backed by a `std::collections::BTreeMap`, kept **private**
24//! behind the [`Storage`] trait. This is the spec `10` §4.3 choice. The crate's
25//! COW B+tree ([`crate::btree`]) was considered to avoid `BTreeMap`'s wasm
26//! codegen, but it is an `OwnedRow` *set* keyed by a `Sort`, not a key→value map —
27//! adapting it costs an encode/decode adapter for a bundle win that is currently
28//! *unmeasured*. Since the backing is invisible behind the trait, we ship the
29//! simplest correct thing now and revisit only if `twiggy` (M9) shows the
30//! `BTreeMap` delta actually matters. Two properties make `BTreeMap` a good fit:
31//! it stores [`StorageValue`] directly (so `get` of a `Take{bound}` is an `Arc`
32//! refcount bump, not a row copy — spec `10` §3.2/§8.1), and it gives the
33//! sorted-key invariant + a native `range` for the prefix [`Storage::scan`].
34
35use std::cell::RefCell;
36use std::collections::BTreeMap;
37use std::ops::Bound;
38use std::rc::Rc;
39
40use crate::error::RindleError;
41use crate::value::OwnedRow;
42
43// ---------------------------------------------------------------------------
44// The value model — a tight enum, NOT serde_json::Value (spec 10 §4.2)
45// ---------------------------------------------------------------------------
46
47/// The value stored in operator scratch state. A **tight enum**, not generic
48/// JSON: the only writers are `Take` and `Cap` (spec `10` §1.3), and their
49/// payloads are known. This (a) keeps `serde_json` *out* of the wasm client,
50/// (b) makes every value a flat, cheaply-clonable struct instead of a heap JSON
51/// tree, and (c) gives operators type-safe state instead of the JS
52/// `storage as TakeStorage` casts (`take.ts:78`).
53///
54/// **No `PartialEq`.** It carries [`OwnedRow`]s ([`crate::value::OwnedValue`]
55/// deliberately has no `PartialEq` — you must choose `compare_values` vs
56/// `values_equal`), so comparison is spelled out where needed (tests compare via
57/// `compare_values`, mirroring `graph::CollectedChange`).
58///
59/// Borrowed and owned forms coincide for this enum (no `&str` fields — `bound` is
60/// already an `OwnedRow`, `pks` are `Box<str>`), so `OwnedStorageValue` is just
61/// an alias; it leaves room for a future borrowed `set` form (spec `10` §4.2).
62#[derive(Clone, Debug)]
63pub enum StorageValue {
64 /// `take.ts` `TakeState`: a count + an optional boundary row. `bound` is an
65 /// `OwnedRow` because it outlives the cursor that produced it (foundations
66 /// §3.3 — operator-buffered state).
67 Take { size: u32, bound: Option<OwnedRow> },
68 /// `take.ts` `MAX_BOUND_KEY` slot: a bare boundary row.
69 Bound(OwnedRow),
70 /// `cap.ts` `CapState`: a count + the membership pk-set (each pk
71 /// pre-serialized to a string exactly as JS `serializePK`, `cap.ts:315`).
72 Cap { size: u32, pks: Vec<Box<str>> },
73 /// `op/reduce.rs` accumulator (`REDUCE-DESIGN.md` §5). `count` is the running row
74 /// count backing `count(*)` (birth/death is driven by it, `NULL`s included). `accs`
75 /// holds one [`ReduceAcc`] per `Sum`/`Avg` aggregate in output-column order (empty for
76 /// a plain `count`), so a `Remove` never re-reads the inputs.
77 Reduce { count: i64, accs: Vec<ReduceAcc> },
78}
79
80/// One per-column running accumulator for a `Sum`/`Avg` aggregate inside a
81/// [`StorageValue::Reduce`] (`REDUCE-DESIGN.md` §5). The integer and float sums are kept
82/// **apart** so the emitted `sum` matches SQLite's typing — integer iff every summed
83/// value was an integer, real once any float contributes — and stays fully invertible
84/// (a later `Remove` of a float value demotes the result back to an integer).
85///
86/// - `int_sum` / `float_sum` — running Σ of the column's integer- and float-typed
87/// non-`NULL` values. `int_sum` is **i128** (design 226 §5.3): the accumulator
88/// folds deltas in arrival order (Removes included), so a transient past
89/// `i64::MAX` that SQLite's scan order would never see must not error or wrap —
90/// and `-1 × i64::MIN` must not overflow on a Remove. Only the **emitted** total
91/// is bounded: `sum` raises a typed error at emit when the set total leaves the
92/// i64 range (`op/reduce.rs`'s `sum_value`).
93/// - `non_null` — count of non-`NULL` values; this is `avg`'s denominator (SQL
94/// `count(col)`, **not** the row `count`), and `avg` emits `NULL` when it is `0`.
95/// - `float_count` — how many contributing values were float; `0` ⇒ `sum` emits `Int`,
96/// else `Float`.
97#[derive(Clone, Debug, Default, PartialEq)]
98pub struct ReduceAcc {
99 pub int_sum: i128,
100 pub float_sum: f64,
101 pub non_null: i64,
102 pub float_count: i64,
103}
104
105/// Owned counterpart returned by [`Storage::get`]/[`Storage::scan`]. Identical to
106/// [`StorageValue`] (the enum has no borrowed fields); aliased for clarity at the
107/// trait surface and to leave room for a future borrowed `set` form (spec `10`
108/// §4.2 / Appendix A).
109pub(crate) type OwnedStorageValue = StorageValue;
110
111// ---------------------------------------------------------------------------
112// The trait (spec 10 §4.1 / Appendix A)
113// ---------------------------------------------------------------------------
114
115/// Per-operator scratch state: a sorted string-keyed map with a prefix range
116/// scan. Ports `zql/src/ivm/operator.ts:132` `Storage`.
117///
118/// **Object-safe by design.** The builder (`08`) stores the chosen backend
119/// behind `Box<dyn Storage>` so the same operator code (`Take`, `Cap`) links
120/// against either backend with no per-backend monomorphization. Operators are
121/// already dyn-dispatched at the graph boundary (foundations §6.2), so the extra
122/// vtable here is free. The boxed, lending-free `scan` return keeps it object-safe.
123///
124/// **`&self`, not `&mut self`** (foundations §6: no `&mut` on operator-held state
125/// during a shared-borrowed reentrant push). Backends use interior mutability.
126/// Storage ops are synchronous and non-reentrant — they never call back into the
127/// graph — so none of the "no borrow across a vend" hazards apply *within* the
128/// store (spec `10` §5, E12).
129///
130/// **The trait is intentionally infallible** (spec `10` §4.1, E14). A backend that
131/// can fail (the SQLite `OpStorage`) reports errors **out of band** — it parks a
132/// [`RindleError`] on the graph's runtime-error sink and returns a safe sentinel
133/// (`None` / empty / no-op); the graph drains it via `take_runtime_error` at the
134/// mutation boundary (WS02.3). The in-RAM [`MemoryStorage`] never fails.
135pub trait Storage {
136 /// Insert or **overwrite** `key`'s value (spec `10` §3.1, E3).
137 fn set(&self, key: &str, value: StorageValue);
138
139 /// Owned lookup. `None` if absent (E1). The JS `get(key, def)` default is a
140 /// caller concern — use `get(k).unwrap_or(default)` (spec `10` §6 deviation
141 /// D1); we do not bake it into the signature. The returned value is **owned**
142 /// (never a borrow into the store), so the operator may hold/mutate it past a
143 /// later `set` regardless of backend (spec `10` §3.2 inv.3, E11).
144 fn get(&self, key: &str) -> Option<OwnedStorageValue>;
145
146 /// Remove `key`. No-op if absent (E4).
147 fn del(&self, key: &str);
148
149 /// Ascending prefix scan. Yields owned `(key, value)` pairs in **byte-ascending
150 /// key order**, starting at the first key `>= prefix` and **stopping at the
151 /// first key not starting with `prefix`** (spec `10` §3.2). `prefix == ""`
152 /// scans the whole keyspace in order (E5/E7).
153 ///
154 /// Returns a boxed iterator of OWNED pairs (the future SQLite backend cannot
155 /// lend across a `step()`; we keep both backends signature-identical — spec
156 /// `10` §4.1, §6).
157 fn scan<'s>(
158 &'s self,
159 prefix: &str,
160 ) -> Box<dyn Iterator<Item = (Box<str>, OwnedStorageValue)> + 's>;
161
162 /// Drop **every** key in this store, leaving it an empty namespace. Called by
163 /// [`Graph::destroy_pipeline`](crate::graph::Graph::destroy_pipeline) to reclaim a
164 /// torn-down operator's scratch state while keeping the slot reusable (the slot —
165 /// and, for a SQLite-backed store, its `op_id` namespace — is recycled by the next
166 /// `alloc_storage`, so we must *clear contents*, not drop the store object).
167 ///
168 /// Default: collect the full keyspace then `del` each (the keys are collected
169 /// first so we are not iterating the store while mutating it). A backend with a
170 /// cheaper bulk delete (e.g. SQLite `DELETE … WHERE op_id = ?`) should override.
171 fn clear(&self) {
172 let keys: Vec<Box<str>> = self.scan("").map(|(k, _)| k).collect();
173 for k in keys {
174 self.del(&k);
175 }
176 }
177}
178
179// ---------------------------------------------------------------------------
180// MemoryStorage (client / tests) — spec 10 §4.3, §5.1
181// ---------------------------------------------------------------------------
182
183/// In-RAM [`Storage`]. A `BTreeMap<Box<str>, StorageValue>` (kept private — see
184/// the module docs) gives the sorted-key invariant, O(log n) point ops, and a
185/// native `range` for the prefix scan. `RefCell` provides the interior mutability
186/// the `&self` trait methods need (single-threaded per pipeline — foundations
187/// §1.3, so no `Sync` required).
188pub struct MemoryStorage {
189 data: RefCell<BTreeMap<Box<str>, StorageValue>>,
190}
191
192impl MemoryStorage {
193 pub fn new() -> MemoryStorage {
194 MemoryStorage {
195 data: RefCell::new(BTreeMap::new()),
196 }
197 }
198
199 /// Test/debug snapshot (ports `cloneData`, `memory-storage.ts:47`). Returns an
200 /// owned copy of the whole map; the `BTreeMap` type is intentionally **not**
201 /// part of the [`Storage`] trait surface, so this leaks only from the concrete
202 /// type, keeping the backing swappable (module docs).
203 pub fn clone_data(&self) -> BTreeMap<Box<str>, StorageValue> {
204 self.data.borrow().clone()
205 }
206}
207
208impl Default for MemoryStorage {
209 fn default() -> MemoryStorage {
210 MemoryStorage::new()
211 }
212}
213
214impl Storage for MemoryStorage {
215 fn set(&self, key: &str, value: StorageValue) {
216 // BTreeMap::insert overwrites an equal key (E3).
217 self.data.borrow_mut().insert(key.into(), value);
218 }
219
220 fn get(&self, key: &str) -> Option<StorageValue> {
221 // `.cloned()` is the owned-copy point (E11). For `Take{bound}`/`Bound` the
222 // "copy" is an `Arc` refcount bump on the `OwnedRow` (spec 10 §3.2).
223 self.data.borrow().get(key).cloned()
224 }
225
226 fn del(&self, key: &str) {
227 // remove returns None for an absent key — no-op, no error (E4).
228 self.data.borrow_mut().remove(key);
229 }
230
231 fn scan<'s>(&'s self, prefix: &str) -> Box<dyn Iterator<Item = (Box<str>, StorageValue)> + 's> {
232 // Collect into a Vec so the RefCell borrow is dropped before the iterator
233 // is handed out: another operator-state op could `set` while the operator
234 // drains the scan, and we must not pin a borrow across that. Scratch-state
235 // scans are tiny (a handful of partition keys), so the copy is negligible;
236 // this mirrors the JS generator over a not-mutated-mid-scan structure
237 // (spec 10 §5.1 borrow discipline, §8.1).
238 let data = self.data.borrow();
239 // `BTreeMap<Box<str>, _>::range` over `Q = str` (since `Box<str>:
240 // Borrow<str>`): the bound is `&str`, no alloc. `Included(prefix)` seeks
241 // `>= prefix` inclusively so an exact prefix key is the first yielded
242 // (E8); `prefix == ""` matches every key (E5). `take_while(starts_with)`
243 // is the exact port of `memory-storage.ts:40` (`if (!key.startsWith(
244 // prefix)) return;`) and bounds the scan to O(matches) (E6).
245 let out: Vec<(Box<str>, StorageValue)> = data
246 .range::<str, _>((Bound::Included(prefix), Bound::Unbounded))
247 .take_while(|(k, _)| k.starts_with(prefix))
248 .map(|(k, v)| (k.clone(), v.clone()))
249 .collect();
250 Box::new(out.into_iter())
251 }
252}
253
254// ---------------------------------------------------------------------------
255// Builder-facing factory (spec 10 §4.5)
256// ---------------------------------------------------------------------------
257
258/// A backend's per-operator store factory, erased behind a trait object so the core
259/// graph's [`StorageFactory`] can vend SQLite-backed operator storage (the
260/// `rindle-sqlite` `DatabaseStorage`) without the core crate naming a `rusqlite`
261/// type. The in-memory path bypasses this (it builds [`MemoryStorage`] directly).
262pub trait StorageProvider {
263 /// Allocate one isolated operator keyspace, parking any backend errors into
264 /// `error_sink` (the graph's `runtime_error`, WS02.3) so they surface via
265 /// `take_runtime_error` instead of aborting.
266 fn create_storage_with_sink(
267 &self,
268 error_sink: Rc<RefCell<Option<RindleError>>>,
269 ) -> Box<dyn Storage>;
270}
271
272pub struct StorageFactory {
273 backend: StorageFactoryBackend,
274}
275
276enum StorageFactoryBackend {
277 Memory,
278 Custom(Rc<dyn StorageProvider>),
279}
280
281impl StorageFactory {
282 pub fn memory() -> StorageFactory {
283 StorageFactory {
284 backend: StorageFactoryBackend::Memory,
285 }
286 }
287
288 /// Build a factory from a custom [`StorageProvider`] (e.g. `rindle-sqlite`'s
289 /// `DatabaseStorage`). Each `alloc_storage` vends one namespaced store.
290 pub fn custom(provider: Rc<dyn StorageProvider>) -> StorageFactory {
291 StorageFactory {
292 backend: StorageFactoryBackend::Custom(provider),
293 }
294 }
295
296 pub fn create_storage(&self) -> Box<dyn Storage> {
297 self.create_storage_with_sink(Rc::default())
298 }
299
300 /// Like [`create_storage`](Self::create_storage) but threads the graph's
301 /// `runtime_error` sink into the backend so operator-storage failures
302 /// surface as `RindleError` via `take_runtime_error` instead of aborting (WS02.3).
303 /// The memory backend ignores the sink (it is infallible).
304 pub(crate) fn create_storage_with_sink(
305 &self,
306 error_sink: Rc<RefCell<Option<RindleError>>>,
307 ) -> Box<dyn Storage> {
308 match &self.backend {
309 StorageFactoryBackend::Memory => {
310 let _ = &error_sink;
311 Box::new(MemoryStorage::new())
312 }
313 StorageFactoryBackend::Custom(provider) => {
314 provider.create_storage_with_sink(error_sink)
315 }
316 }
317 }
318}
319
320impl Default for StorageFactory {
321 fn default() -> StorageFactory {
322 StorageFactory::memory()
323 }
324}
325
326/// What the builder (`08`) calls to give a stateful operator its own store. On
327/// the client/test path a *fresh object is the namespace* (spec `10` §3.3, §4.5):
328/// each `Take`/`Cap` gets a disjoint keyspace because it gets a distinct
329/// `MemoryStorage`. The JS `name` argument is not needed by the memory backend
330/// (uniqueness is object identity — spec `10` §6 deviation D2), so it is not taken
331/// here; the builder keeps `name` at its own boundary for debug/logging.
332///
333/// The server path should inject a [`StorageFactory`] built from
334/// `DatabaseStorage`; this compatibility helper stays memory-backed.
335pub fn create_storage() -> Box<dyn Storage> {
336 Box::new(MemoryStorage::new())
337}