Rindle docs and package mapSkip to main content

Module stmt_cache

Module stmt_cache 

Source
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] does cache.remove(sql) — the statement is out of the cache while in use, so a concurrent same-SQL get cannot alias it and prepares a fresh one. This is the JS “remove on get” semantics.
  • The returned [rusqlite::CachedStatement] returns itself to the cache on Drop — RAII return, strictly better than the JS manual finally → cache.return() discipline (table-source.ts:340-374): an early break, a ?, or early return all return the statement for free (foundations §7, README Primitive #2). This cache is sqlite-only (native server): a panic returns the statement only in unwinding builds; the release-server profile (panic = "unwind") covers panic, but a plain release build (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:

  1. 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.
  2. LRU eviction (bounded), not manual drop(n) — the JS cache is unbounded with an externally-driven drop(n) (statement-cache.ts:53-69). rusqlite’s cache is a bounded LRU; StatementCache::set_capacity tunes 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.
  3. 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 by build_select_query (05 §4.4) in canonical single-spaced form, so normalization would be a wasted per-get allocation 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§

PooledStmt
RAII handle owning a checked-out prepared statement (05 §4.3, the PooledStmt). On Drop it 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 JS get/finally → cache.return() and the rowIterator.return?.() cursor-reset (table-source.ts:340-374); early break, ?, and early return all trigger it. A panic triggers it only in unwinding builds (this is sqlite-only server code): the release-server profile (panic = "unwind") covers panic, but a plain release build (panic = "abort") aborts and the Drop does not run. See WS02.
StatementCache
The prepared-statement cache for a SQLite source (05 §4.3). Owns the connection (Rc<Connection>, mirroring the spec’s Rc<Db> so the cache can live alongside the per-snapshot write statements and be rebound on a Snapshotter leapfrog, 05 §5.7) and lends out PooledStmt RAII guards.

Constants§

DEFAULT_CAPACITY
rusqlite’s default per-connection statement-cache capacity (LRU slots). We adopt it unless StatementCache::with_capacity overrides — a server with many distinct fetch shapes may want a larger bound (05 §13 Q7).