sqlite: add DBStatus wrapper for sqlite3_db_status

Follow-up to the !130 (merged) review, where we agreed to expose sqlite3_db_status as a public API so the pcache pool's I/O can be measured against pcache1 with the real pager counters instead of the EasyRefusals proxy.

API (the part to review)

New dbstatus.go, mirroring the FileControl surface you pointed at:

type DBStatusOp int32

type DBStatus interface {
    Status(op DBStatusOp, reset bool) (current, high int, err error)
}
var _ DBStatus = (*conn)(nil)
  • DBStatusOp is a distinct typed enum of the SQLITE_DBSTATUS_* verbs, the same way FetchMode is typed, so a constant from another op family will not compile in its place.
  • All 14 ops the transpiled lib/ defines are exposed (DBStatusLookasideUsed, DBStatusCacheUsed, DBStatusSchemaUsed, DBStatusStmtUsed, DBStatusLookasideHit, DBStatusLookasideMissSize, DBStatusLookasideMissFull, DBStatusCacheHit, DBStatusCacheMiss, DBStatusCacheWrite, DBStatusDeferredFKs, DBStatusCacheUsedShared, DBStatusCacheSpill, DBStatusTempbufSpill).
  • Status is implemented on the unexported *conn and reached through (*sql.Conn).Raw(), exactly like FileControl. It uses the tls.Alloc(8) two-int32 pattern from your skeleton and surfaces an out-of-range op as an error via c.errstr(rc).
  • The doc comments call out the three counter families that report their value differently: memory high-water ops (*Used), running-counter ops (CacheHit/Miss/Write/Spill, high == 0, reset zeroes current), and the lookaside event ops (LookasideHit/MissSize/MissFull) whose count is reported in high, not current.

Usage:

err := sqlConn.Raw(func(dc any) error {
    cur, _, err := dc.(sqlite.DBStatus).Status(sqlite.DBStatusCacheSpill, false)
    ...
})

Tests

dbstatus_test.go drives all three families through Raw(): SchemaUsed grows after DDL; CacheHit resets; LookasideHit reports its value in high not current; an out-of-range op errors.

Benchmark (the application that motivated the API)

pcache/bench_test.go gains BenchmarkPoolSpillIO, which reads the pager-level CACHE_SPILL / CACHE_WRITE / CACHE_HIT / CACHE_MISS counters through the new API instead of the in-package EasyRefusals proxy. These counters are maintained identically by the pager for pcache1 and the pool, so the comparison is apples-to-apples, which is the measurement you asked for on the !127 (merged) review. As with the existing pcache benchmarks the pcache1 baseline comes from running the same workload in a tree that does not import pcache (registration is process-global and one-shot).

Numbers on the rotating-residue eviction-churn workload at cache_size=16, darwin/arm64, flat across 25x/50x/100x:

metric pcache1 pool
cache-spill/op 8.96 31.96
cache-write/op 436 450
cache-hit/op 1021 1021
cache-miss/op 61.9 62.3

The pool spills ~3.5x more than pcache1 for ~3% more actual page writes at identical hit/miss. That quantifies the I/O cost of the strict Easy contract (refusing to recycle at cap) that EasyRefusals only proxied: most extra spill decisions are absorbed, but the spill-decision rate itself is materially higher. Honest trade-off for the off-Go-heap page memory the pool buys.

Verification (darwin/arm64)

  • gofmt, go vet (only the pre-existing Buf/Extra notices), staticcheck all clean on the new files
  • go test -run TestDBStatus and go test -race -short ./pcache/ pass
  • cross-build clean across linux/{amd64,386,arm,arm64} + darwin/{amd64,arm64} + windows/amd64 + freebsd/amd64

CHANGELOG added to the pending v1.53.0 section.

The BenchmarkPoolSpillIO numbers are also a candidate to fold back into the pcache benchmark comment now that the real counters supersede the EasyRefusals proxy framing, but I left the proxy in place for now since it is cheap and in-package; happy to do that as a follow-up if you prefer.

Merge request reports

Loading