6c92f85d10
Bring the Nexus/Praxis/Hexis integration in line with MAVEN_ECOSYSTEM_ARCHITECTURE.md: - Praxis over HTTP: drop the in-process praxis.db open (praxisstore/ praxistools) and call praxisd's /api/v1/tools/* API via a new praxisClient. Honors the "no component reads another's DB" invariant (AC#12). PraxisConfig.DBPath -> URL. - Hexis confirmation gate: mutating capabilities (ReadOnly=false) now park a bound pendingHexis confirmation and require a spoken "да" before executing; read-only run immediately (AC#7, no auto attention->action). - Capability safety: >1 verb match is ambiguous -> ask instead of firing the first; ambiguous Nexus resolution asks for clarification (AC#2). - Correlation IDs on Hexis execute, recorded in the cross-service trace. - Bug: importance arrives as JSON float64 over HTTP, not int. - Tests: confirm-gate, decline, read-only, and ambiguity paths. Build: vendor/ bakes in the hexis client (replace-directed at a sibling repo outside the Docker context); Dockerfile builds from vendor and no longer `go mod download`s the unreachable replace paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
389 lines
13 KiB
Go
389 lines
13 KiB
Go
// Copyright 2026 The Sqlite Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package sqlite
|
|
|
|
import (
|
|
"math/bits"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"modernc.org/libc"
|
|
"modernc.org/libc/sys/types"
|
|
sqlite3 "modernc.org/sqlite/lib"
|
|
)
|
|
|
|
// The pcache binding owns the sqlite3_pcache_page stub that SQLite sees
|
|
// and forwards user-facing calls to the registered PageCache and
|
|
// per-database Cache instances. The user-facing surface in
|
|
// pagecache.go is idiomatic Go; this file holds the package-internal
|
|
// trampolines that bridge to the cgo-free C ABI.
|
|
//
|
|
// Ownership model:
|
|
//
|
|
// - Each xCreate on the C side mints a fresh pcacheBinding and a
|
|
// uintptr handle from pcacheBindings.gen. SQLite stores the handle
|
|
// as its opaque sqlite3_pcache*; trampolines recover the binding
|
|
// via pcacheBindings.m under RLock.
|
|
// - Each xFetch on the C side maps a (binding, key) pair to a
|
|
// sqlite3_pcache_page stub allocated via libc.Xcalloc. The stub
|
|
// lives until Unpin(discard=true), a Truncate sweep covering the
|
|
// key, or Destroy on the owning cache. SQLite re-fetches for the
|
|
// same key return the same stub; this is the binding-side fix for
|
|
// the "stale pPgHdr->pPage" issue called out in MR #126's
|
|
// §2 design note.
|
|
// - The Page object the user returns from Cache.Fetch is held in
|
|
// the binding's byStub map so a Page-identity comparison on the
|
|
// next Fetch for the same key can short-circuit stub reallocation
|
|
// when the impl retains the entry.
|
|
//
|
|
// Threading invariant: all Cache callbacks for a single pcacheBinding
|
|
// are serialised by the SQLite engine. This driver opens every
|
|
// connection SQLITE_OPEN_FULLMUTEX without shared-cache mode, and
|
|
// database/sql never invokes one driver.Conn from two goroutines, so
|
|
// the Cache instance returned by xCreate is only ever touched from
|
|
// one goroutine at a time. The mutex on pcacheBinding protects the
|
|
// maps against any future shared-cache mode and against the
|
|
// cross-cache traffic that the package-global pcacheBindings
|
|
// registry would otherwise impose; under the current invariant it is
|
|
// uncontended.
|
|
type pcacheBinding struct {
|
|
mu sync.Mutex
|
|
cache Cache
|
|
byKey map[uint32]uintptr // key -> sqlite3_pcache_page stub uintptr
|
|
byStub map[uintptr]pcacheEntry // stub uintptr -> {Page, key}
|
|
}
|
|
|
|
type pcacheEntry struct {
|
|
page Page
|
|
key uint32
|
|
}
|
|
|
|
// pcacheBindings is the package-global registry mapping the opaque
|
|
// sqlite3_pcache* handles SQLite sees to the *pcacheBinding they
|
|
// represent. Handles are minted from a bitset-backed allocator so they
|
|
// can be reclaimed on Destroy and stay small across the process
|
|
// lifetime (otherwise a long-running process that opens and closes many
|
|
// caches would grow the map without bound).
|
|
var pcacheBindings = struct {
|
|
mu sync.RWMutex
|
|
m map[uintptr]*pcacheBinding
|
|
gen pcacheIDGen
|
|
}{
|
|
m: map[uintptr]*pcacheBinding{},
|
|
}
|
|
|
|
// lookupPcacheBinding returns the binding for the given C handle, or
|
|
// nil if the handle is unknown. It is called from every trampoline
|
|
// other than Init/Shutdown.
|
|
func lookupPcacheBinding(h uintptr) *pcacheBinding {
|
|
pcacheBindings.mu.RLock()
|
|
defer pcacheBindings.mu.RUnlock()
|
|
return pcacheBindings.m[h]
|
|
}
|
|
|
|
// registerPcacheBinding installs b into the registry and returns the
|
|
// opaque handle SQLite will see.
|
|
func registerPcacheBinding(b *pcacheBinding) uintptr {
|
|
pcacheBindings.mu.Lock()
|
|
defer pcacheBindings.mu.Unlock()
|
|
h := pcacheBindings.gen.next()
|
|
pcacheBindings.m[h] = b
|
|
return h
|
|
}
|
|
|
|
// unregisterPcacheBinding removes h from the registry and reclaims its
|
|
// ID slot. Called from pcacheTrampolineDestroy after the user's
|
|
// Destroy returns.
|
|
func unregisterPcacheBinding(h uintptr) {
|
|
pcacheBindings.mu.Lock()
|
|
defer pcacheBindings.mu.Unlock()
|
|
delete(pcacheBindings.m, h)
|
|
pcacheBindings.gen.reclaim(h)
|
|
}
|
|
|
|
// pcacheIDGen is a bitset-backed allocator over uintptr handles
|
|
// starting at 1 (0 is reserved as a sentinel for "not registered").
|
|
// Identical in shape to the vtab idGen at sqlite.go:808-824; reproduced
|
|
// here to keep the pcache subsystem self-contained.
|
|
type pcacheIDGen struct {
|
|
bitset []uint64
|
|
}
|
|
|
|
func (g *pcacheIDGen) next() uintptr {
|
|
base := uintptr(1)
|
|
for i := 0; i < len(g.bitset); i, base = i+1, base+64 {
|
|
b := g.bitset[i]
|
|
if b != 1<<64-1 {
|
|
n := uintptr(bits.TrailingZeros64(^b))
|
|
g.bitset[i] |= 1 << n
|
|
return base + n
|
|
}
|
|
}
|
|
g.bitset = append(g.bitset, 1)
|
|
return base
|
|
}
|
|
|
|
func (g *pcacheIDGen) reclaim(id uintptr) {
|
|
bit := id - 1
|
|
g.bitset[bit/64] &^= 1 << (bit % 64)
|
|
}
|
|
|
|
// Sentinel returned by the binding when SQLite asks for a stub but the
|
|
// user impl returned nil or the binding could not allocate. Translating
|
|
// to uintptr(0) tells SQLite to fall back to its stress path.
|
|
const pcacheNullStub uintptr = 0
|
|
|
|
// pcacheStubSize is the size of the sqlite3_pcache_page struct that
|
|
// SQLite reads: two uintptr fields (pBuf, pExtra). Computed at startup
|
|
// to keep the trampolines free of unsafe.Sizeof calls on the hot path.
|
|
var pcacheStubSize = types.Size_t(unsafe.Sizeof(uintptr(0)) * 2)
|
|
|
|
// pcacheTrampolineInit is wired to sqlite3_pcache_methods2.xInit. The
|
|
// binding has no per-process state to set up at sqlite3_initialize
|
|
// time; PageCache.Create is invoked per database, not per
|
|
// initialize, and the package-global pcacheBindings registry is ready
|
|
// from package init. Returns SQLITE_OK unconditionally.
|
|
func pcacheTrampolineInit(tls *libc.TLS, arg uintptr) int32 {
|
|
return sqlite3.SQLITE_OK
|
|
}
|
|
|
|
// pcacheTrampolineShutdown is wired to xShutdown. The binding does not
|
|
// call sqlite3_shutdown internally, so in normal operation this
|
|
// trampoline never fires; it exists so the C methods table never
|
|
// holds a null xShutdown slot when paired with custom user code that
|
|
// might trigger sqlite3_shutdown out of band.
|
|
func pcacheTrampolineShutdown(tls *libc.TLS, arg uintptr) {}
|
|
|
|
// pcacheTrampolineCreate is wired to xCreate. SQLite asks the binding
|
|
// to create a new per-database cache. The trampoline calls
|
|
// PageCache.Create on the registered module and registers the
|
|
// returned Cache in pcacheBindings, handing SQLite back the opaque
|
|
// handle.
|
|
//
|
|
// szPage and szExtra are the sizes SQLite wants for each page's Buf
|
|
// and Extra buffers; szExtra is NOT just the application-visible
|
|
// extraSize, it already includes SQLite's PgHdr overhead. The
|
|
// Cache implementation must honour it as opaque.
|
|
//
|
|
// bPurgeable is advisory: 0 for in-memory databases (Unpin will only
|
|
// ever be called with discard=true), non-zero otherwise.
|
|
//
|
|
// A return of 0 tells SQLite the cache could not be created (treated
|
|
// as SQLITE_NOMEM by the caller in _sqlite3PcacheSetPageSize).
|
|
func pcacheTrampolineCreate(tls *libc.TLS, szPage, szExtra, bPurgeable int32) uintptr {
|
|
module := pcacheState.registered
|
|
if module == nil {
|
|
// Defensive: should be impossible because SQLite only consults
|
|
// the methods table after _sqlite3Config.Fpcache2 is installed,
|
|
// which happens only after registered is set.
|
|
return pcacheNullStub
|
|
}
|
|
cache, err := module.Create(int(szPage), int(szExtra), bPurgeable != 0)
|
|
if err != nil || cache == nil {
|
|
return pcacheNullStub
|
|
}
|
|
b := &pcacheBinding{
|
|
cache: cache,
|
|
byKey: map[uint32]uintptr{},
|
|
byStub: map[uintptr]pcacheEntry{},
|
|
}
|
|
return registerPcacheBinding(b)
|
|
}
|
|
|
|
// pcacheTrampolineCachesize forwards SQLite's xCachesize to
|
|
// Cache.SetSize. The size is advisory.
|
|
func pcacheTrampolineCachesize(tls *libc.TLS, pCache uintptr, nCachesize int32) {
|
|
b := lookupPcacheBinding(pCache)
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.cache.SetSize(int(nCachesize))
|
|
}
|
|
|
|
// pcacheTrampolinePagecount forwards xPagecount to Cache.PageCount.
|
|
func pcacheTrampolinePagecount(tls *libc.TLS, pCache uintptr) int32 {
|
|
b := lookupPcacheBinding(pCache)
|
|
if b == nil {
|
|
return 0
|
|
}
|
|
return int32(b.cache.PageCount())
|
|
}
|
|
|
|
// pcacheTrampolineFetch forwards xFetch to Cache.Fetch on every
|
|
// request. SQLite does not require that re-fetches for the same key
|
|
// return an identical sqlite3_pcache_page pointer:
|
|
// _sqlite3PcacheFetchFinish keys off whether pPage->pExtra is already
|
|
// initialised (PgHdr.pPage != 0); a fresh page (zeroed Extra) is just
|
|
// re-initialised. So the binding asks the impl on every call and only
|
|
// reuses the cached stub when the returned Page is the same value;
|
|
// when the impl evicts and replaces the entry the stale stub is
|
|
// retired and a fresh one minted.
|
|
//
|
|
// This makes a bounded, evicting purgeable cache (the normal way an
|
|
// impl honours cache_size) safe by construction: an impl that drops
|
|
// an entry on Unpin(discard=false) cannot leak a stale stub to
|
|
// SQLite because the next Fetch reaches the impl, observes the
|
|
// eviction (page == nil or a different Page), and the binding
|
|
// updates its bookkeeping accordingly.
|
|
func pcacheTrampolineFetch(tls *libc.TLS, pCache uintptr, key uint32, createFlag int32) uintptr {
|
|
b := lookupPcacheBinding(pCache)
|
|
if b == nil {
|
|
return pcacheNullStub
|
|
}
|
|
page := b.cache.Fetch(key, FetchMode(createFlag))
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
old, had := b.byKey[key]
|
|
|
|
if page == nil {
|
|
// Lookup miss, eviction, or OOM: retire any stale stub the
|
|
// binding still held for this key. The next Fetch with
|
|
// createFlag > 0 will re-allocate.
|
|
if had {
|
|
delete(b.byKey, key)
|
|
delete(b.byStub, old)
|
|
libc.Xfree(tls, old)
|
|
}
|
|
return pcacheNullStub
|
|
}
|
|
if had && b.byStub[old].page == page {
|
|
// Same Page value retained across the unpin/refetch cycle;
|
|
// reuse the existing stub. Buf and Extra addresses are stable
|
|
// while the page is pinned, which is exactly the window during
|
|
// which SQLite holds the stub pointer.
|
|
return old
|
|
}
|
|
if had {
|
|
// Page was replaced for this key (impl evicted and re-allocated
|
|
// fresh memory). Retire the stale stub before minting a new one;
|
|
// SQLite has already finished using the old stub by the time
|
|
// xUnpin returned.
|
|
delete(b.byStub, old)
|
|
libc.Xfree(tls, old)
|
|
}
|
|
stub := libc.Xcalloc(tls, 1, pcacheStubSize)
|
|
if stub == 0 {
|
|
delete(b.byKey, key)
|
|
return pcacheNullStub
|
|
}
|
|
f := (*[2]uintptr)(unsafe.Pointer(stub))
|
|
f[0], f[1] = uintptr(page.Buf()), uintptr(page.Extra())
|
|
b.byKey[key] = stub
|
|
b.byStub[stub] = pcacheEntry{page: page, key: key}
|
|
return stub
|
|
}
|
|
|
|
// pcacheTrampolineUnpin forwards xUnpin to Cache.Unpin. When discard
|
|
// is non-zero the binding evicts: it frees the stub after the user's
|
|
// Unpin returns. When discard is zero the binding only forwards to
|
|
// the user; the cache MAY release or retain the page and its memory
|
|
// at its discretion. The next Fetch for the key consults the impl,
|
|
// so a release between unpin and the next fetch is observed and
|
|
// reflected in the binding's bookkeeping.
|
|
func pcacheTrampolineUnpin(tls *libc.TLS, pCache, pPage uintptr, discard int32) {
|
|
b := lookupPcacheBinding(pCache)
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
entry, ok := b.byStub[pPage]
|
|
b.mu.Unlock()
|
|
if !ok {
|
|
return
|
|
}
|
|
b.cache.Unpin(entry.page, discard != 0)
|
|
if discard == 0 {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
delete(b.byKey, entry.key)
|
|
delete(b.byStub, pPage)
|
|
b.mu.Unlock()
|
|
libc.Xfree(tls, pPage)
|
|
}
|
|
|
|
// pcacheTrampolineRekey forwards xRekey to Cache.Rekey, then updates
|
|
// the binding's byKey map so a subsequent Fetch for newKey returns
|
|
// the same stub. If newKey already has a stub the spec guarantees
|
|
// the colliding entry is unpinned; the binding retires it.
|
|
func pcacheTrampolineRekey(tls *libc.TLS, pCache, pPage uintptr, oldKey, newKey uint32) {
|
|
b := lookupPcacheBinding(pCache)
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
entry, ok := b.byStub[pPage]
|
|
b.mu.Unlock()
|
|
if !ok {
|
|
return
|
|
}
|
|
b.cache.Rekey(entry.page, oldKey, newKey)
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if collider, ok := b.byKey[newKey]; ok && collider != pPage {
|
|
delete(b.byKey, newKey)
|
|
delete(b.byStub, collider)
|
|
libc.Xfree(tls, collider)
|
|
}
|
|
delete(b.byKey, oldKey)
|
|
b.byKey[newKey] = pPage
|
|
entry.key = newKey
|
|
b.byStub[pPage] = entry
|
|
}
|
|
|
|
// pcacheTrampolineTruncate forwards xTruncate to Cache.Truncate
|
|
// and then sweeps every binding-owned stub whose key is greater than
|
|
// or equal to limit. SQLite documents Truncate as the only callback
|
|
// permitted to evict a pinned page, so the binding does the eviction
|
|
// unconditionally.
|
|
func pcacheTrampolineTruncate(tls *libc.TLS, pCache uintptr, iLimit uint32) {
|
|
b := lookupPcacheBinding(pCache)
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.cache.Truncate(iLimit)
|
|
b.mu.Lock()
|
|
for key, stub := range b.byKey {
|
|
if key >= iLimit {
|
|
delete(b.byKey, key)
|
|
delete(b.byStub, stub)
|
|
libc.Xfree(tls, stub)
|
|
}
|
|
}
|
|
b.mu.Unlock()
|
|
}
|
|
|
|
// pcacheTrampolineDestroy forwards xDestroy to Cache.Destroy,
|
|
// frees every remaining stub, and removes the binding from the
|
|
// registry. After this trampoline returns the SQLite handle is no
|
|
// longer valid and the binding instance is unreachable.
|
|
func pcacheTrampolineDestroy(tls *libc.TLS, pCache uintptr) {
|
|
b := lookupPcacheBinding(pCache)
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.cache.Destroy()
|
|
b.mu.Lock()
|
|
for _, stub := range b.byKey {
|
|
libc.Xfree(tls, stub)
|
|
}
|
|
b.byKey = nil
|
|
b.byStub = nil
|
|
b.mu.Unlock()
|
|
unregisterPcacheBinding(pCache)
|
|
}
|
|
|
|
// pcacheTrampolineShrink forwards xShrink to Cache.Shrink. SQLite
|
|
// calls this as a memory-pressure hint; the user impl is free to
|
|
// ignore it.
|
|
func pcacheTrampolineShrink(tls *libc.TLS, pCache uintptr) {
|
|
b := lookupPcacheBinding(pCache)
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.cache.Shrink()
|
|
}
|