Expand description
The prepared-statement cache (spec 05 §4.3) — a thin wrapper over
rusqlite’s built-in per-connection statement cache, not a from-scratch port
of internal/statement-cache.ts.
§Why wrap instead of port
The JS StatementCache exists to solve one problem: a SQLite prepared
statement is not reentrant — a single statement cannot be iterated by two
callers at once (statement-cache.ts:16-20). The JS solves it by removing a
statement from the cache on get and pushing it back on return; a
concurrent get for the same SQL (the self-join / reentrant-fetch case, 05
§3.10) prepares a fresh statement so both callers iterate independently.
rusqlite ships exactly this discipline already (rusqlite::cache):
- [
Connection::prepare_cached] doescache.remove(sql)— the statement is out of the cache while in use, so a concurrent same-SQLgetcannot alias it and prepares a fresh one. This is the JS “remove on get” semantics. - The returned [
rusqlite::CachedStatement] returns itself to the cache onDrop— RAII return, strictly better than the JS manualfinally → cache.return()discipline (table-source.ts:340-374): an earlybreak, a?, or early return all return the statement for free (foundations §7, README Primitive #2). This cache issqlite-only (native server): a panic returns the statement only in unwinding builds; therelease-serverprofile (panic = "unwind") covers panic, but a plainreleasebuild (panic = "abort") aborts the process and no destructor runs. See WS02.
Critically, the built-in cache dissolves the spec’s #1 implementation risk
(05 §13 Q1, the rusqlite::Statement<'static> self-referential-struct
problem): it stores a lifetime-free RawStatement (a raw FFI pointer) and
re-attaches the connection borrow only for the checkout window. That raw
mechanism is pub(crate) in rusqlite, so a faithful multi-slot port could
not reuse it — it would have to reach for an unsafe 'static transmute or
a self-referencing dependency (ouroboros), i.e. take on the exact risk the
spec flagged. Standing on prepare_cached is the lower-risk path.
§Deviations from the JS cache (noted, per 05 §13 Q7)
The spec explicitly says do not silently change the eviction policy and then assume the same eviction order in tests. The deviations here are deliberate:
- Single statement per SQL key, LRU-evicted — not the JS
Map<sql, Statement[]>(multiple cached copies per SQL for concurrent reuse). Under a reentrant self-join where the outer and inner fetch emit identical SQL (constraint values are bound params,05§4.4, so the text is identical), the single slot re-prepares one extra statement per reentrant cycle and finalizes the surplus on return. A real but modest hot-path cost, measurable at the §11 benchmarking milestone; if it bites, the upgrade to a multi-slot front is local to this module. We do not pre-optimize into the §13 Q1 unsafe territory before a benchmark justifies it. - LRU eviction (bounded), not manual
drop(n)— the JS cache is unbounded with an externally-drivendrop(n)(statement-cache.ts:53-69). rusqlite’s cache is a bounded LRU;StatementCache::set_capacitytunes the bound. This resolves §13 Q7 (the memory-footprint risk) without a pipeline-driver hook. Eviction order differs from the JS — any differential test that asserted the JS FIFO-ish trim order must be rewritten against LRU. - No internal whitespace normalization on the hot path — the JS
normalizeWhitespace(statement-cache.ts:129) collapses internal whitespace runs so hand-written SQL hits the same key. All fetch SQL here is emitted bybuild_select_query(05§4.4) in canonical single-spaced form, so normalization would be a wasted per-getallocation on the hot path. The cache assumes canonical SQL; rusqlite still trims leading/trailing whitespace for the key. (If a future caller feeds non-canonical SQL, normalize once when building it, not on every cache hit.)
Structs§
- Pooled
Stmt - RAII handle owning a checked-out prepared statement (
05§4.3, thePooledStmt). OnDropit returns the statement to the per-connection cache — which also resets the cursor, so the next write does not hit “database is busy” — and decrements the open-cursor count. This replaces both the JSget/finally → cache.return()and therowIterator.return?.()cursor-reset (table-source.ts:340-374); earlybreak,?, and early return all trigger it. A panic triggers it only in unwinding builds (this issqlite-only server code): therelease-serverprofile (panic = "unwind") covers panic, but a plainreleasebuild (panic = "abort") aborts and theDropdoes not run. See WS02. - Statement
Cache - The prepared-statement cache for a SQLite source (
05§4.3). Owns the connection (Rc<Connection>, mirroring the spec’sRc<Db>so the cache can live alongside the per-snapshot write statements and be rebound on a Snapshotter leapfrog,05§5.7) and lends outPooledStmtRAII guards.
Constants§
- DEFAULT_
CAPACITY - rusqlite’s default per-connection statement-cache capacity (LRU slots). We
adopt it unless
StatementCache::with_capacityoverrides — a server with many distinct fetch shapes may want a larger bound (05§13 Q7).