rindle/memory_source.rs
1//! The client-side leaf [`Source`]: a `Source` backed entirely by in-RAM COW
2//! B+trees (`btree.rs`). Production port of `packages/zql/src/ivm/memory-source.ts`
3//! (`class MemorySource`). It owns three things (`04` §1.1):
4//!
5//! 1. **Multiple COW B+tree indexes** — a `Sort → Index` map. The primary index
6//! is keyed by `(pk asc, …)`; others are built lazily from the primary when a
7//! connection requests a sort the source doesn't have, and are **kept for the
8//! source's lifetime** (never GC'd — §3.10).
9//! 2. **Connections** — one per downstream, each with its own `sort`, filters,
10//! and split-edit keys ([`Connection`], in `source_common` since the shape is
11//! backend-identical).
12//! 3. **The overlay / push / edit-split machinery** — fan a change out to every
13//! connection (overlay live, epoch-gated), clear the overlay, then write to
14//! every index. This machinery is the backend-agnostic
15//! [`source_common`] seam, shared verbatim with the
16//! SQLite `TableSource`.
17//!
18//! It is the zero-alloc-to-vend showcase: the B+tree stores `OwnedRow`s; the leaf
19//! scan vends each as a borrowed `&OwnedRow` through the lending
20//! [`RowStream`], and a row is only owned-cloned (an
21//! `Arc` refcount bump) when it escapes into a `Node`, an overlay, or the view.
22
23use std::cell::{Cell, RefCell};
24use std::collections::HashMap;
25use std::rc::Rc;
26
27use crate::btree::{row_bound_of, BTree, Bound, RowBound};
28use crate::change::{
29 constraint_matches, constraint_matches_primary_key, merge_constraints, Constraint,
30 FetchRequest, MultiConstraint, OutEdge, RowFlow, SourceChange,
31};
32use crate::error::RindleError;
33use crate::graph::{ConnId, Source};
34use crate::push_index::PushGuard;
35use crate::source_common::{
36 self, generate_with_constraint, generate_with_filter, generate_with_overlay,
37 generate_with_start, merge_sorted_streams, ConnTable, Connection, ConnectionFilters,
38};
39use crate::value::{
40 compare_rows, owned_row, ColId, OwnedRow as Row, OwnedValue, RowRef, RowStream, Schema, Sort,
41};
42
43/// Cap on how many distinct values a WHERE-derived seek will drive
44/// ([`MemorySource::fetch_by_guard`]). Each value costs one index seek, one heap slot
45/// in the k-way merge and one overlay clone, so past a few dozen the merge costs more
46/// than the ordered scan it replaces; above the cap the fetch falls back to that scan.
47/// A single-cell spatial subscription (`where cell = n`) is 1.
48const MAX_SEEK_VALUES: usize = 64;
49
50/// One index: a COW B+tree of `OwnedRow`s under a sort-specific comparator.
51/// Mirrors JS `Index` (`memory-source.ts:69`) MINUS `usedBy` (the index-GC that
52/// was removed — §4.1 DEVIATION: with no GC, `usedBy` is dead bookkeeping).
53pub struct Index {
54 pub sort: Sort,
55 pub data: BTree,
56}
57
58/// The in-memory source. `fetch` seeks a [`BTreeCursor`](crate::btree::BTreeCursor)
59/// over a COW snapshot and drives it through [`source_common`]; `write_change`
60/// path-copies via `Rc::make_mut` so in-flight cursors keep their snapshot (the
61/// load-bearing COW property — §6).
62pub struct MemorySource {
63 /// Static table metadata (columns + PK). The per-connection sort lives on each
64 /// [`Connection`]; the schema's own `sort` is reported to downstream operators
65 /// that need a child ordering (e.g. join relationship streams).
66 pub schema: Schema,
67 primary_key: Vec<ColId>,
68 /// `(pk[0] asc, pk[1] asc, …)` — the primary index sort.
69 primary_index_sort: Sort,
70 /// `Sort → Index`. Keyed by the resolved `Sort` itself (not the JS
71 /// `JSON.stringify(sort)` — `Sort: Hash + Eq`). KEPT for the source's lifetime.
72 indexes: RefCell<HashMap<Sort, Index>>,
73 conns: ConnTable,
74 /// The single in-flight change, epoch-tagged. Read LIVE via a tightly-scoped
75 /// borrow during a reentrant fetch, cloned out, cleared after the push drains
76 /// BEFORE the write (§6.3).
77 overlay: RefCell<Option<source_common::Overlay>>,
78 epoch: Cell<u32>,
79 /// Primitive #2 demo: RAII cursor count. Incremented when a fetch stream is
80 /// created, decremented by its `Drop` (the analogue of a SQLite cursor).
81 cursors_open: Rc<Cell<i64>>,
82}
83
84impl MemorySource {
85 /// Validate that every ingest row matches the schema's column width (WS02.4): a
86 /// wrong-width row is rejected here with a [`RindleError::SchemaViolation`] rather
87 /// than reaching the engine's unchecked column index ([`crate::value::RowRef::col`])
88 /// where it would panic / abort.
89 pub fn validate_rows(schema: &Schema, rows: &[Row]) -> Result<(), RindleError> {
90 let want = schema.columns.len();
91 for (i, r) in rows.iter().enumerate() {
92 if r.len() != want {
93 return Err(RindleError::schema_violation(format!(
94 "ingest row {i} has {} cells but the schema declares {want} columns",
95 r.len()
96 )));
97 }
98 }
99 Ok(())
100 }
101
102 /// Build a source over `initial` rows, **validating** each row's width against
103 /// the schema first (WS02.4). The primary index is keyed by `(pk asc)`; `initial`
104 /// is sorted under it and bulk-loaded (`from_sorted`, O(N)). Callers guarantee
105 /// `initial` has unique primary keys (a set). Prefer this over
106 /// [`MemorySource::new`] / [`crate::graph::Graph::add_source`] in production.
107 pub fn try_new(schema: Schema, initial: Vec<Row>) -> Result<MemorySource, RindleError> {
108 Self::validate_rows(&schema, &initial)?;
109 let primary_key = schema.primary_key.clone();
110 let primary_index_sort: Sort = primary_key.iter().map(|&c| (c, true)).collect();
111 let mut data = initial;
112 data.sort_by(|a, b| compare_rows(&primary_index_sort, a, b));
113 let tree = BTree::from_sorted(data.into_iter(), &primary_index_sort);
114 let mut indexes = HashMap::new();
115 indexes.insert(
116 primary_index_sort.clone(),
117 Index {
118 sort: primary_index_sort.clone(),
119 data: tree,
120 },
121 );
122 Ok(MemorySource {
123 schema,
124 primary_key,
125 primary_index_sort,
126 indexes: RefCell::new(indexes),
127 conns: ConnTable::new(),
128 overlay: RefCell::new(None),
129 epoch: Cell::new(0),
130 cursors_open: Rc::new(Cell::new(0)),
131 })
132 }
133
134 /// Panicking convenience wrapper over [`MemorySource::try_new`] (tests /
135 /// prototyping).
136 ///
137 /// # Panics
138 /// If a row's width does not match the schema. Prefer [`MemorySource::try_new`]
139 /// or [`crate::graph::Graph::try_add_source`] in production (WS02.6).
140 pub fn new(schema: Schema, initial: Vec<Row>) -> MemorySource {
141 Self::try_new(schema, initial).expect("MemorySource::new: invalid ingest rows")
142 }
143
144 /// O(1) `fork` (`memory-source.ts:135`): a new source sharing the primary
145 /// index's COW root (one `Rc` bump, no deep copy). Other indexes are NOT
146 /// carried (they rebuild lazily on demand, as in the JS).
147 pub fn fork(&self) -> MemorySource {
148 let primary = self.indexes.borrow();
149 let data = primary
150 .get(&self.primary_index_sort)
151 .expect("primary index")
152 .data
153 .fork();
154 let mut indexes = HashMap::new();
155 indexes.insert(
156 self.primary_index_sort.clone(),
157 Index {
158 sort: self.primary_index_sort.clone(),
159 data,
160 },
161 );
162 MemorySource {
163 schema: self.schema.clone(),
164 primary_key: self.primary_key.clone(),
165 primary_index_sort: self.primary_index_sort.clone(),
166 indexes: RefCell::new(indexes),
167 conns: ConnTable::new(),
168 overlay: RefCell::new(None),
169 epoch: Cell::new(0),
170 cursors_open: Rc::new(Cell::new(0)),
171 }
172 }
173
174 /// Like [`fork`](Self::fork), but COW-carries **every** index (each a one-`Rc` bump on its
175 /// B+tree root), not just the primary — the `203-MUTATOR-READS-DESIGN.md` §7.3 (A) read-cache
176 /// optimization. A mutator's one-shot `tx.query` that sorts in an order a live query already
177 /// built then shares that secondary index by COW lineage, collapsing its O(n log n) lazy
178 /// build to a bump. The carried indexes are kept current by
179 /// [`apply_change`](Self::apply_change) (it writes every index), so seeding the cache fork
180 /// (buffer replay) and write-forwarding maintain them. Used **only** for the off-graph
181 /// read-cache fork and its per-query fork-of-fork; the plain [`fork`](Self::fork) stays
182 /// primary-only for the optimistic `sync` baselines and the other COW snapshots that want the
183 /// lighter, divergence-bounded fork (`OPTIMISTIC-WRITES-DESIGN.md` §1.2).
184 pub fn fork_with_indexes(&self) -> MemorySource {
185 let src = self.indexes.borrow();
186 let mut indexes = HashMap::with_capacity(src.len());
187 for (sort, index) in src.iter() {
188 indexes.insert(
189 sort.clone(),
190 Index {
191 sort: index.sort.clone(),
192 data: index.data.fork(),
193 },
194 );
195 }
196 MemorySource {
197 schema: self.schema.clone(),
198 primary_key: self.primary_key.clone(),
199 primary_index_sort: self.primary_index_sort.clone(),
200 indexes: RefCell::new(indexes),
201 conns: ConnTable::new(),
202 overlay: RefCell::new(None),
203 epoch: Cell::new(0),
204 cursors_open: Rc::new(Cell::new(0)),
205 }
206 }
207
208 /// Test hook: the set of index sorts currently held (indexes persist across
209 /// `destroy` — §3.10). Mirrors `getIndexKeys` (`memory-source.ts:252`).
210 pub fn get_index_keys(&self) -> Vec<Sort> {
211 self.indexes.borrow().keys().cloned().collect()
212 }
213
214 /// An O(1) COW fork of the **primary** index tree (one `Rc` bump). The optimistic
215 /// loop's `sync`/`S'` trees are forks of this — sharing the live tree's node
216 /// lineage is what keeps `structural_diff` bounded by the divergence
217 /// (`OPTIMISTIC-WRITES-DESIGN.md` §1.2).
218 pub fn fork_primary(&self) -> BTree {
219 self.indexes
220 .borrow()
221 .get(&self.primary_index_sort)
222 .expect("primary index")
223 .data
224 .fork()
225 }
226
227 /// The primary-index sort (`(pk[0] asc, pk[1] asc, …)`) — the comparator every
228 /// fork/diff over [`fork_primary`](Self::fork_primary) trees must use.
229 pub fn primary_sort(&self) -> &Sort {
230 &self.primary_index_sort
231 }
232
233 /// The current row whose primary key equals `probe`'s (other columns ignored by
234 /// the pk-only primary sort), or `None`. The optimistic `MutationTx` read path.
235 pub fn get_by_pk(&self, probe: &Row) -> Option<Row> {
236 self.indexes
237 .borrow()
238 .get(&self.primary_index_sort)
239 .expect("primary index")
240 .data
241 .get(probe, &self.primary_index_sort)
242 .cloned()
243 }
244
245 pub fn cursors_open(&self) -> i64 {
246 self.cursors_open.get()
247 }
248
249 /// Total connection slots, including freed (recyclable) ones. Stays bounded by the
250 /// peak live-connection count under query churn (the slot is recycled on teardown —
251 /// it does NOT grow per teardown). For metrics/tests.
252 pub fn conn_count(&self) -> usize {
253 self.conns.len()
254 }
255
256 /// Number of live downstream connections (readers) — 0 once every pipeline reading this
257 /// source has been torn down. See [`ConnTable::live_conn_count`].
258 pub fn live_conn_count(&self) -> usize {
259 self.conns.live_conn_count()
260 }
261
262 /// Wire a connection's downstream edge (mirrors `input.setOutput`).
263 pub fn set_conn_output(&self, conn: ConnId, edge: OutEdge) {
264 self.conns.set_output(conn, edge);
265 }
266
267 /// The connection slots a write would fan out to — the push index's candidate
268 /// set (`designs/205`). A test probe for the guarded / dynamic-guard pruning.
269 pub fn push_candidates(&self, change: &SourceChange) -> Vec<u32> {
270 self.conns.push_candidates(change)
271 }
272
273 /// The push index's entry count (see `ConnTable::push_index_size`) — a churn probe.
274 pub fn push_index_size(&self) -> usize {
275 self.conns.push_index_size()
276 }
277
278 // -- push orchestration (the source owns its state; the graph drives output) --
279
280 /// Eager push: fan `change` to every connection (overlay live, epoch-gated),
281 /// clear the overlay, then write to every index — via the backend-agnostic
282 /// `source_common::gen_push_and_write_with_split_edit`. `push_one` is the
283 /// graph's downstream driver (it reads the connection's output edge). Edit-
284 /// splitting, existence asserts, and the write-after-drain ordering all live
285 /// in `source_common`. `push_one` receives a `SourceChange` (rows); the graph
286 /// turns it into a node-bearing downstream `Change` at the connection boundary.
287 pub fn push(&self, change: SourceChange, push_one: &dyn Fn(&Connection, SourceChange)) {
288 source_common::gen_push_and_write_with_split_edit(
289 &self.conns,
290 change,
291 &|row| self.exists(row),
292 &|o| *self.overlay.borrow_mut() = o,
293 &|c| self.write_change(c),
294 &|| {
295 let e = self.epoch.get() + 1;
296 self.epoch.set(e);
297 e
298 },
299 push_one,
300 );
301 }
302
303 /// Fallible push used by strict change validation (WS02.2). Identical fan-out to
304 /// [`MemorySource::push`] but routed through the fallible `source_common` sibling
305 /// so a malformed change (when `strict`) returns a
306 /// [`RindleError::ConsistencyViolation`]. The memory `exists`/`write` are infallible,
307 /// so they wrap in `Ok`.
308 pub fn try_push(
309 &self,
310 change: SourceChange,
311 push_one: &dyn Fn(&Connection, SourceChange),
312 strict: bool,
313 ) -> Result<(), RindleError> {
314 source_common::try_gen_push_and_write_with_split_edit(
315 &self.conns,
316 change,
317 &|row| Ok(self.exists(row)),
318 &|o| *self.overlay.borrow_mut() = o,
319 &|c| {
320 self.write_change(c);
321 Ok(())
322 },
323 &|| {
324 let e = self.epoch.get() + 1;
325 self.epoch.set(e);
326 e
327 },
328 push_one,
329 strict,
330 )
331 }
332
333 /// `exists` = primary-index `has` (`memory-source.ts:450`).
334 fn exists(&self, row: &Row) -> bool {
335 self.indexes
336 .borrow()
337 .get(&self.primary_index_sort)
338 .expect("primary index")
339 .data
340 .has(row, &self.primary_index_sort)
341 }
342
343 /// Apply ADD/REMOVE/EDIT to **every** index (`#writeChange`,
344 /// `memory-source.ts:463`). Each index is a separate COW B+tree; EDIT is
345 /// delete-old + add-new (cannot `set` — old/row may differ in position). The
346 /// same row `Arc` is stored in every index (only the tree structure differs).
347 fn write_change(&self, change: &SourceChange) {
348 let mut indexes = self.indexes.borrow_mut();
349 for index in indexes.values_mut() {
350 match change {
351 SourceChange::Add(r) => {
352 let ok = index.data.add(r.clone(), &index.sort);
353 debug_assert!(ok, "MemorySource: ADD must be newly inserted");
354 }
355 SourceChange::Remove(r) => {
356 let ok = index.data.delete(r, &index.sort);
357 debug_assert!(ok, "MemorySource: REMOVE must have been present");
358 }
359 SourceChange::Edit { row, old } => {
360 let ok = index.data.delete(old, &index.sort);
361 debug_assert!(ok, "MemorySource: EDIT old must have been present");
362 index.data.add(row.clone(), &index.sort);
363 }
364 }
365 }
366 }
367
368 /// Apply ADD/REMOVE/EDIT to every index **without** the live push path's existence
369 /// asserts and **without** the connection fan-out — for the off-graph read-cache
370 /// fork of `203-MUTATOR-READS-DESIGN.md` (mutator reads). A read-cache fork has no
371 /// connections, so there is nothing to fan out to; this is purely the index write
372 /// that keeps the fork equal to `live ⊕ this txn's buffer` for its table (§4 / §4.1):
373 /// it seeds the fork (replay of the table's buffered ops) and write-forwards each
374 /// later staged op. It is deliberately tolerant of the degenerate "edit a row that
375 /// isn't there" case the staging path defers to commit (`WriteTxn::edit`), so a
376 /// later `tx.query` cannot panic on it.
377 pub fn apply_change(&self, change: &SourceChange) {
378 let mut indexes = self.indexes.borrow_mut();
379 for index in indexes.values_mut() {
380 match change {
381 SourceChange::Add(r) => {
382 index.data.add(r.clone(), &index.sort);
383 }
384 SourceChange::Remove(r) => {
385 index.data.delete(r, &index.sort);
386 }
387 SourceChange::Edit { row, old } => {
388 index.data.delete(old, &index.sort);
389 index.data.add(row.clone(), &index.sort);
390 }
391 }
392 }
393 }
394
395 // -- lazy index management --
396
397 /// Build the index for `index_sort` from the primary index's rows if it does
398 /// not exist yet (`#getOrCreateIndex`, `memory-source.ts:225`). O(N) bulk load
399 /// (`from_sorted`), never repeated `add`. Indexes are never removed (§3.10).
400 fn ensure_index(&self, index_sort: &Sort) {
401 if self.indexes.borrow().contains_key(index_sort) {
402 return;
403 }
404 // Own each primary row (an `Arc` bump — the new index shares the primary's
405 // row `Arc`s), re-sort under the new comparator, bulk-load.
406 let mut rows: Vec<Row> = {
407 let indexes = self.indexes.borrow();
408 let primary = indexes
409 .get(&self.primary_index_sort)
410 .expect("primary index");
411 let mut cur = primary
412 .data
413 .values_from(None, true, &self.primary_index_sort);
414 let mut v = Vec::with_capacity(primary.data.len());
415 while let Some(r) = cur.next_row() {
416 v.push(r.to_owned_row());
417 }
418 v
419 };
420 rows.sort_by(|a, b| compare_rows(index_sort, a, b));
421 let data = BTree::from_sorted(rows.into_iter(), index_sort);
422 self.indexes.borrow_mut().insert(
423 index_sort.clone(),
424 Index {
425 sort: index_sort.clone(),
426 data,
427 },
428 );
429 }
430
431 // -- fetch --
432
433 /// The multiConstraint path (`#fetchMulti`, `memory-source.ts:380`): drive a
434 /// sub-fetch per entry of the FIRST multiConstraint (merging base ∧ entry),
435 /// k-way-merge the sorted sub-streams, then post-filter against the rest
436 /// (keep iff the row matches SOME entry of EVERY remaining multiConstraint).
437 /// This is the one place the memory leaf diverges from `05` (which lowers to
438 /// native SQL `IN`): it pays one index seek per primary entry.
439 fn fetch_multi<'g>(&'g self, conn: ConnId, req: &FetchRequest) -> RowFlow<'g> {
440 let multis: Vec<&MultiConstraint> = req
441 .multi_constraints
442 .iter()
443 .filter(|mc| !mc.is_empty())
444 .collect();
445 // `has_multi` guarantees `multis` is non-empty.
446 let primary = multis[0];
447 let rest: Vec<MultiConstraint> = multis[1..].iter().map(|m| (*m).clone()).collect();
448 let base = req.constraint.clone();
449
450 let sub_streams: Vec<RowFlow<'g>> = primary
451 .iter()
452 .map(|c| {
453 let merged = merge_constraints(base.as_ref(), c);
454 let sub_req = FetchRequest {
455 constraint: Some(merged),
456 multi_constraints: Vec::new(),
457 start: req.start.clone(),
458 reverse: req.reverse,
459 };
460 self.fetch(conn, &sub_req)
461 })
462 .collect();
463
464 let sort = self.conns.sort(conn);
465 let merged = merge_sorted_streams(sub_streams, sort, req.reverse);
466
467 if rest.is_empty() {
468 return merged;
469 }
470 Box::new(merged.filter(move |row| {
471 rest.iter()
472 .all(|mc| mc.iter().any(|c| constraint_matches(row, c)))
473 }))
474 }
475
476 /// The WHERE-derived seek (`fetch` step 0): turn the connection's own
477 /// [`PushGuard`] into an index seek, or `None` to fall through to the plain
478 /// ordered scan.
479 ///
480 /// Sound because the guard is an **implication** — `predicate(row) => row[col]
481 /// IN values` (`builder::extract_push_guard`). A row outside `values` cannot
482 /// satisfy the predicate, so restricting the scan to those values' runs drops no
483 /// row the full scan would have kept, and any *over*-approximating guard stays
484 /// correct (the exact predicate still runs downstream). Same "err only toward
485 /// more" direction the push index relies on, read here as "err only toward
486 /// scanning more" — see the [`push_index`](crate::push_index) module docs.
487 ///
488 /// The guard already exists on every connection; before this it was used ONLY to
489 /// prune the push fan-out (`designs/205`). Reading with it is the same fact
490 /// spent twice — but NOT under the same comparator, which is the one trap here:
491 /// the push index buckets guard values under `values_identical` (null == null),
492 /// while a fetch constraint matches under `values_equal` (null != null). Any
493 /// guard carrying a NULL is therefore refused below rather than reinterpreted.
494 fn fetch_by_guard<'g>(
495 &'g self,
496 conn: ConnId,
497 req: &FetchRequest,
498 guard: &PushGuard,
499 ) -> Option<RowFlow<'g>> {
500 // A never-matching guard (`col = NULL`, an empty `IN`, an all-false `OR`) is
501 // encoded as EMPTY values. It must NOT go through `multi_constraints`:
502 // `FetchRequest::has_multi` reads an empty disjunction as *absent*, so the
503 // fetch would silently fall back to a full scan. Answer it here instead.
504 if guard.values.is_empty() {
505 return Some(Box::new(std::iter::empty()));
506 }
507 // A NULL guard value cannot become a fetch constraint. `col IS NULL` guards on
508 // `values = [Null]`, which is right for the predicate (`values_identical`:
509 // null == null) and for the push index (`GuardKey`, same identity) — but a
510 // fetch constraint is matched with `values_equal`, where **null != null** (join
511 // semantics, deliberately not interchangeable — `rindle_value::value`). The
512 // seek would land correctly and then `generate_with_constraint`'s `take_while`
513 // would reject the very first row and end the stream at zero. Fall back to the
514 // plain scan; `IS NULL` is simply not an accelerable shape here.
515 if guard.values.iter().any(|v| v.is_null()) {
516 return None;
517 }
518 if guard.values.len() > MAX_SEEK_VALUES {
519 return None;
520 }
521 let multi: MultiConstraint = guard
522 .values
523 .iter()
524 .map(|v| vec![(guard.col, v.clone())])
525 .collect();
526 Some(self.fetch_multi(
527 conn,
528 &FetchRequest {
529 constraint: None,
530 multi_constraints: vec![multi],
531 start: req.start.clone(),
532 reverse: req.reverse,
533 },
534 ))
535 }
536}
537
538/// `assertOrderingIncludesPK` (`complete-ordering.ts`): an ordered connection's
539/// sort must include every primary-key column (so sort-equality ⇒ identity — the
540/// overlay remove-suppression and view diffing depend on it).
541fn assert_ordering_includes_pk(sort: &Sort, pk: &[ColId]) {
542 for &p in pk {
543 assert!(
544 sort.iter().any(|(c, _)| *c == p),
545 "connection ordering must include the primary key column {p}",
546 );
547 }
548}
549
550/// The index sort a constrained ordered fetch scans under (`memory-source.ts`):
551/// constraint columns (asc) then the connection sort — unless a single-column pk is
552/// fully constrained (at most one result, so no sort tail is needed). `pub` because
553/// the SQLite batch delta (`rindle-sqlite`) seeks its per-transaction rows under the
554/// SAME rule — the two must agree on the seek structure or merged fetches misorder.
555pub fn constrained_index_sort(
556 constraint: Option<&Constraint>,
557 sort: &Sort,
558 primary_key: &[ColId],
559) -> Sort {
560 let mut index_sort: Sort = Vec::new();
561 if let Some(c) = constraint {
562 for (col, _) in c {
563 index_sort.push((*col, true));
564 }
565 }
566 if primary_key.len() > 1
567 || constraint.is_none()
568 || !constraint_matches_primary_key(constraint.unwrap(), primary_key)
569 {
570 index_sort.extend(sort.iter().copied());
571 }
572 index_sort
573}
574
575/// Build the scan-start `RowBound` for a constraint (`memory-source.ts:326`):
576/// each constrained column → its value; each unconstrained index column → the
577/// type-extreme sentinel that sorts to the *near* end, reverse-aware
578/// (forward: asc→Min/desc→Max; reverse: asc→Max/desc→Min). `pub` for the same
579/// reason as [`constrained_index_sort`]: the batch delta's seek shares it.
580pub fn build_scan_start(c: &Constraint, index_sort: &Sort, reverse: bool) -> RowBound {
581 index_sort
582 .iter()
583 .map(|&(col, asc)| match c.iter().find(|(cc, _)| *cc == col) {
584 Some((_, v)) => (col, Bound::Val(v.clone())),
585 None => {
586 let sentinel = if reverse {
587 if asc {
588 Bound::Max
589 } else {
590 Bound::Min
591 }
592 } else if asc {
593 Bound::Min
594 } else {
595 Bound::Max
596 };
597 (col, sentinel)
598 }
599 })
600 .collect()
601}
602
603impl crate::scalar::ScalarSource for MemorySource {
604 fn schema(&self) -> &Schema {
605 &self.schema
606 }
607
608 /// PK-only for the slice — the only statically-unique key a memory source has
609 /// (its other indexes are query-derived ordering structures, not uniqueness
610 /// constraints; `SCALAR-SUBQUERY-DESIGN.md` §4.3).
611 fn unique_keys(&self) -> Vec<Vec<ColId>> {
612 vec![self.primary_key.clone()]
613 }
614
615 fn lookup_unique(&self, bound: &[(ColId, OwnedValue)]) -> Option<Row> {
616 // The caller guarantees `bound` covers the PK; build a full-width probe row
617 // (non-PK cells are `Null` — `get_by_pk` only compares the PK columns).
618 let mut cells = vec![OwnedValue::Null; self.schema.columns.len()];
619 for (col, val) in bound {
620 cells[*col] = val.clone();
621 }
622 self.get_by_pk(&owned_row(cells))
623 }
624}
625
626impl Source for MemorySource {
627 fn connect(
628 &self,
629 sort: Option<Sort>,
630 filters: Option<ConnectionFilters>,
631 split_edit_keys: Vec<ColId>,
632 ) -> ConnId {
633 let unordered = sort.is_none();
634 let internal_sort = sort.unwrap_or_else(|| self.primary_index_sort.clone());
635 if !unordered {
636 assert_ordering_includes_pk(&internal_sort, &self.primary_key);
637 }
638 self.conns.connect(Connection {
639 sort: internal_sort,
640 unordered,
641 split_edit_keys,
642 filters,
643 last_pushed_epoch: Cell::new(0),
644 output: Cell::new(None),
645 })
646 }
647
648 fn schema(&self) -> &Schema {
649 &self.schema
650 }
651
652 fn conn_sort(&self, conn: ConnId) -> Sort {
653 self.conns.sort(conn)
654 }
655
656 fn destroy(&self, conn: ConnId) {
657 // Drop the connection's output edge so it stops receiving pushes (and RECYCLE
658 // its slot — the `ConnTable` free-list); KEEP all indexes (the deliberate JS
659 // decision — §3.10).
660 self.conns.destroy(conn);
661 }
662
663 fn cursors_open(&self) -> i64 {
664 MemorySource::cursors_open(self)
665 }
666
667 fn try_push(
668 &self,
669 change: SourceChange,
670 push_one: &dyn Fn(&Connection, SourceChange),
671 strict: bool,
672 ) -> Result<(), RindleError> {
673 MemorySource::try_push(self, change, push_one, strict)
674 }
675
676 /// The in-memory backend is infallible — a fetch never parks an error.
677 fn take_error(&self) -> Option<RindleError> {
678 None
679 }
680
681 fn set_conn_output(&self, conn: ConnId, edge: OutEdge) {
682 MemorySource::set_conn_output(self, conn, edge)
683 }
684
685 fn add_guard_value(&self, conn: ConnId, value: OwnedValue) {
686 self.conns.add_guard_value(conn, value)
687 }
688
689 fn remove_guard_value(&self, conn: ConnId, value: &OwnedValue) {
690 self.conns.remove_guard_value(conn, value)
691 }
692
693 /// `#fetch` (the hot read path — `memory-source.ts:257`). Pick an index, seek
694 /// a `scanStart` `RowBound`, then drive the lending cursor through the
695 /// generator chain: overlay (index comparator) → start (connection comparator)
696 /// → constraint (`break`) → filter. The two comparators differ by design when
697 /// a constraint is present (§3.1). Emits **rows**, not nodes — the connection
698 /// boundary (`Graph::fetch` on the `SourceConn`) wraps each row in a leaf node.
699 fn fetch<'g>(&'g self, conn: ConnId, req: &FetchRequest) -> RowFlow<'g> {
700 if req.has_multi() {
701 return self.fetch_multi(conn, req);
702 }
703 let idx = self.conns.live_ix(conn);
704
705 // Snapshot the connection's needed bits (short borrow, cloned out — no
706 // RefCell borrow held across the vend).
707 let (sort, predicate, pk_constraint, seek_guard, last_epoch) = {
708 let conns = self.conns.borrow();
709 let c = &conns[idx];
710 (
711 c.sort.clone(),
712 c.filters.as_ref().map(|f| f.predicate.clone()),
713 c.filters.as_ref().and_then(|f| f.pk_constraint.clone()),
714 c.filters.as_ref().and_then(|f| f.push_guard.clone()),
715 c.last_pushed_epoch.get(),
716 )
717 };
718
719 // 0. WHERE-DERIVED SEEK. With no caller constraint, the connection's own
720 // `where` can still bound the scan: the `PushGuard` states
721 // `predicate(row) => row[col] IN values`, so every matching row lives in
722 // those values' runs of a `(col asc, ...sort)` index and seeking only
723 // them loses nothing. Routed through `multi_constraints` (never a
724 // `pk_constraint`-style seek slot) so the sub-request carries a REAL
725 // `constraint`: that is what makes the seek, the `take_while` break, and
726 // the overlay narrowing all read the same bound — the three agree only
727 // on this path (`fetch_multi` -> `merge_constraints`). Re-entry is
728 // self-limiting: the sub-request has `constraint = Some(..)`, so this
729 // arm cannot fire again.
730 if pk_constraint.is_none() && req.constraint.is_none() {
731 if let Some(g) = &seek_guard {
732 if let Some(stream) = self.fetch_by_guard(conn, req, g) {
733 return stream;
734 }
735 }
736 }
737
738 // The PK constraint (from filters) is more limiting than req.constraint.
739 let fetch_or_pk: Option<&Constraint> = pk_constraint.as_ref().or(req.constraint.as_ref());
740
741 // 1. INDEX SORT: constraint cols (asc) then requested sort — unless a
742 // single-column PK is fully constrained (at most one result).
743 let index_sort = constrained_index_sort(fetch_or_pk, &sort, &self.primary_key);
744
745 // 2. INDEX: fetch-or-build (lazy; kept for the source's lifetime).
746 self.ensure_index(&index_sort);
747
748 // 3. scanStart RowBound (constraint-seek; sentinels for unconstrained cols).
749 // With no constraint, seek to the plain start row (the basis is applied
750 // by generate_with_start, not the seek).
751 let scan_start: Option<RowBound> = match fetch_or_pk {
752 Some(c) => Some(build_scan_start(c, &index_sort, req.reverse)),
753 None => req
754 .start
755 .as_ref()
756 .map(|s| row_bound_of(&s.row, &index_sort)),
757 };
758
759 // 4. SCAN (lending; zero per-row alloc). Always inclusive.
760 let cursor = {
761 let indexes = self.indexes.borrow();
762 let index = indexes.get(&index_sort).expect("ensure_index built it");
763 if req.reverse {
764 index
765 .data
766 .values_from_reversed(scan_start.as_ref(), true, &index_sort)
767 } else {
768 index
769 .data
770 .values_from(scan_start.as_ref(), true, &index_sort)
771 }
772 };
773
774 // RAII guard (Primitive #2): bundled with the cursor — the innermost,
775 // statically dispatched layer — so the count balances to zero on Drop
776 // (even if a downstream `Take` abandons the stream early) without an
777 // extra boxed wrapper on every fetch.
778 let cursor = GuardedRows {
779 rows: cursor,
780 _guard: CursorGuard::new(self.cursors_open.clone()),
781 };
782
783 // 5. Snapshot the epoch-gated overlay (tight borrow, cloned out).
784 let overlay = self.overlay.borrow().clone();
785
786 // 6. The generator chain. The overlay splice interleaves under the INDEX
787 // comparator (`break`-safety, §3.1); the start gate — and therefore the
788 // overlay's start NARROWING, which must agree with it — uses the
789 // CONNECTION comparator (`overlays_for_start_at`'s invariant: with a
790 // start row from outside the scan's constraint region, an index-sort
791 // comparison is decided by the foreign constraint columns and drops the
792 // in-flight edit — the take-over-flipped-fetch bound loss).
793 // Both use `req.constraint` (NOT fetch_or_pk — the PK constraint from
794 // filters acts as a fetch seek + a row filter, not the overlay
795 // constraint).
796 let start_at = req.start.as_ref().map(|s| s.row.clone());
797 let mut stream = generate_with_overlay(
798 start_at.as_ref(),
799 cursor,
800 req.constraint.as_ref(),
801 overlay.as_ref(),
802 last_epoch,
803 &index_sort,
804 &sort,
805 req.reverse,
806 predicate.as_ref(),
807 &[],
808 );
809 stream = generate_with_start(stream, req.start.clone(), sort, req.reverse);
810 stream = generate_with_constraint(stream, req.constraint.clone());
811 if let Some(p) = predicate {
812 stream = generate_with_filter(stream, p);
813 }
814 stream
815 }
816}
817
818// ---------------------------------------------------------------------------
819// RAII cursor guard (Primitive #2): Drop == JS finally/.return()
820// ---------------------------------------------------------------------------
821
822struct CursorGuard(Rc<Cell<i64>>);
823impl CursorGuard {
824 fn new(c: Rc<Cell<i64>>) -> Self {
825 c.set(c.get() + 1);
826 CursorGuard(c)
827 }
828}
829impl Drop for CursorGuard {
830 fn drop(&mut self) {
831 self.0.set(self.0.get() - 1);
832 }
833}
834
835/// Bundles the cursor-count guard with the leaf cursor itself (mirroring the
836/// SQLite leaf, where `OwnedSqliteRows` owns its guard), so dropping the
837/// generator chain that owns the cursor — even after partial consumption, a
838/// downstream `Take` breaking early — releases the "cursor" with no boxed
839/// wrapper. The Rust analogue of the JS `for-of` calling `generator.return()`
840/// → `finally`, deterministic and free.
841struct GuardedRows<S> {
842 rows: S,
843 _guard: CursorGuard,
844}
845impl<S: RowStream> RowStream for GuardedRows<S> {
846 type Row<'a>
847 = S::Row<'a>
848 where
849 Self: 'a;
850 fn next_row(&mut self) -> Option<Self::Row<'_>> {
851 self.rows.next_row()
852 }
853}