Expand description
Primitive #4 — the COW B+tree (the clean win). Spike for spec
04-sources-memory-and-btree.md §4.4 (the M3 milestone) and the two open
questions it flags as the #1 implementation risk:
-
OQ-1 —
BTreeCursorself-reference / snapshot wiring (§4.4.4, §12 Q1). The cursor must hold itsRc<BNode>snapshot and vend a borrowed&OwnedRowout of it. The naive&'t BNodefield that borrows another field of the same struct is a self-referential struct — not expressible in safe Rust. This spike locks in the recommended shape: the cursor ownsRc<BNode>clones along the spine (no&'tself-borrows) andnext_rowreturns&self.leaf.keys[pos], a borrow ofselfvalid only until the next call — exactly the lendingRowStreamcontract. Result: the cursor needs no lifetime parameter at all. -
OQ-2 — owning a row at the node boundary without a deep copy (§8.2, §12 Q2). Node construction needs the stored
Arc<[OwnedValue]>to bump (refcount++), not deep-copy. A bare borrowed slice has lost itsArcidentity, soArc::from(slice)would re-allocate. Resolution — folded into the shared trait: the cursor’sRowStream::Row<'a>is&'a OwnedRow(not a slice), soRowRef::to_owned_rowis justArc::cloneof the stored row — a refcount bump, never a copy (proven byArc::ptr_eqin the tests). No concretecurrent_arcescape hatch is needed: the sameto_owned_rowthe SQLite leaf uses to copy out of its step buffer is, here, a bump.
What Rc<BNode> + Rc::make_mut deletes (§4.4.1): the JS
BNode.isShared transitive flag (btree-set.ts:384-396) — the hardest
hand-port artifact — vanishes. Sharing is Arc::strong_count > 1 (tracked
by the runtime); “clone before mutate if shared” is precisely Rc::make_mut;
transitivity falls out because make_mut is called lazily per node as the
mutation descends, copying only the root→leaf path and only the nodes that are
actually aliased. fork()/clone() is one Arc bump — O(1).
Scope note: this uses the spike crate’s existing owned Value/Row
(value.rs); the production Value<'a>/OwnedValue split is Step 2 (a
mechanical type swap — the algorithm here is the production one). add,
delete (with the JS opportunistic tryMerge rebalancing), from_sorted, the
forward/reverse bounded cursor, and fork/make_mut COW are all faithful
ports of shared/src/btree-set.ts and differential-tested against it
(tests/btree_diff.rs). The one deliberate divergence from JS is the
set “shift-to-avoid-split” sibling rebalance (btree-set.ts:558-583): it is
a pure micro-optimization (never changes contents, and is never triggered by
ascending inserts or from_sorted, so it affects no benchmark), and it carries
the most bug surface of any path — omitted on purpose, documented in
node_set. In production this module is #[cfg(feature = "memory")].
Structs§
- BTree
- A copy-on-write B+tree set of
OwnedRows, ordered by aSortcomparator passed per call (no stored closure — foundations §11.6). ReplacesBTreeSet<Row>(btree-set.ts:7) withRc<BNode>+Rc::make_mut. The comparator lives with the enclosingIndexin the real source; the spike threads&Sort. - BTree
Cursor - A forward-or-reverse ordered cursor over a
BTree. No lifetime parameter (the OQ-1 result): it OWNSRc<BNode>clones along the spine + the root snapshot, so it borrows nothing externally; the only borrow is the onenext_rowlends out ofselfper call. Holding the_snapshotrootArckeeps the entire pre-write tree alive and immutable for the cursor’s life — a concurrentBTree::add/deletemake_muts a fresh path (because the snapshot pinsstrong_count > 1) and never touches these nodes. That is what makes reentrant fetch-during-push safe (§6, foundations §6.3). - Diff
Stats - Work counters for the structural diff — the performance oracle (design
§2.2.2). A
fork + N point-mutationsdiff must show O(N·height) visits and O(N) value compares; a regression (lost node sharing, orptr_eqnot firing on some target) surfaces as visits proportional to tree size instead.
Enums§
- Bound
- A scan-start cell: a concrete value or a type-extreme sentinel that sorts
below/above every real value (including
Null, the lowest real value). Mirrors JSBound = Value | minValue | maxValue(memory-source.ts:967). The spike uses one owned form (the productionBound<'a>/OwnedBoundsplit is the zero-copy concern of Step 2).
Traits§
- Diff
Sink - Sink for
BTree::diff_visit: one callback per difference, emitted in descending sort order. “this” isself; “other” is the argument tree. - RowRef
- A borrowed view of one row; columns addressed by
ColId. The returnedValueborrowsself, so it is valid only as long as the row reference — for the SQLite leaf that means until the nextnext_row. - RowStream
- A lending stream of rows.
next_rowreborrowsself, so the row it returns is invalidated by the next call — exactly the SQLite cursor contract, now a compile-time invariant. This is the leaf source’s output shape; it is notstd::iter::Iterator(which hands out owned items and cannot express the borrow). GAT-based lending, stable since Rust 1.65.
Functions§
- row_
bound_ of - Build an all-
ValRowBoundfrom a concrete lower-bound row.