Rindle docs and package mapSkip to main content

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}