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>
360 lines
15 KiB
Go
360 lines
15 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 "modernc.org/sqlite"
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"unsafe"
|
|
|
|
"modernc.org/libc"
|
|
"modernc.org/libc/sys/types"
|
|
sqlite3 "modernc.org/sqlite/lib"
|
|
)
|
|
|
|
// ErrPageCacheTooLate is returned by RegisterPageCache when a SQLite
|
|
// connection has already been opened in this process. SQLITE_CONFIG_PCACHE2
|
|
// must be installed before sqlite3_initialize, which is called implicitly
|
|
// by the first sqlite3_open_v2. After that point SQLite returns
|
|
// SQLITE_MISUSE and the engine cannot switch its page cache backend.
|
|
var ErrPageCacheTooLate = errors.New(
|
|
"sqlite: RegisterPageCache called after first Open; " +
|
|
"SQLITE_CONFIG_PCACHE2 must be installed before any connection is opened")
|
|
|
|
// ErrPageCacheConflict is returned when a different PageCache has
|
|
// already been registered in this process. The same module value may be
|
|
// re-registered without error, which lets multiple library imports share
|
|
// a singleton without coordination.
|
|
var ErrPageCacheConflict = errors.New(
|
|
"sqlite: a different page cache module is already registered")
|
|
|
|
// FetchMode tells Cache.Fetch how aggressively to allocate when the
|
|
// requested key is absent. It matches the createFlag of SQLite's xFetch
|
|
// (https://sqlite.org/c3ref/pcache_methods2.html).
|
|
type FetchMode int32
|
|
|
|
const (
|
|
// FetchLookup looks up an existing entry. Fetch returns nil if the
|
|
// key is not in cache; no allocation is performed.
|
|
FetchLookup FetchMode = 0
|
|
// FetchCreateEasy allocates only if it can be done without effort
|
|
// (no eviction, no memory pressure). Fetch may return nil; SQLite
|
|
// will then spill dirty pages and retry with FetchCreateForce.
|
|
FetchCreateEasy FetchMode = 1
|
|
// FetchCreateForce allocates unconditionally and may evict to make
|
|
// room. Fetch should return nil only on a genuine out-of-memory
|
|
// condition.
|
|
FetchCreateForce FetchMode = 2
|
|
)
|
|
|
|
// PageCache is the factory for per-database Cache instances.
|
|
// SQLite calls Create once per open database; each call must return a
|
|
// fresh Cache with the given pageSize and extraSize. The extraSize
|
|
// includes SQLite's private PgHdr overhead and must be honoured as the
|
|
// opaque size of every Page's Extra buffer.
|
|
//
|
|
// purgeable is advisory: when false (in-memory databases), SQLite will
|
|
// only call Unpin with discard=true and the cache is permitted to free
|
|
// every page on Unpin. When true, the cache may retain unpinned pages
|
|
// for re-use.
|
|
type PageCache interface {
|
|
Create(pageSize, extraSize int, purgeable bool) (Cache, error)
|
|
}
|
|
|
|
// Cache is one database's worth of cached pages. All callbacks for a
|
|
// single Cache 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 an implementation does not need to synchronise per-Cache state
|
|
// against concurrent calls.
|
|
//
|
|
// Implementations should NOT call RegisterPageCache directly or
|
|
// transitively. Callbacks run under the openGate read lock that the
|
|
// Open path holds, and a re-entrant Register would deadlock on the
|
|
// gate's write lock.
|
|
type Cache interface {
|
|
// SetSize advises the cache of the new target page count
|
|
// (PRAGMA cache_size). The cache is free to ignore the hint.
|
|
SetSize(n int)
|
|
|
|
// PageCount returns the number of pages currently held (pinned and
|
|
// unpinned combined).
|
|
PageCount() int
|
|
|
|
// Fetch returns the Page for key or nil per FetchMode. The binding
|
|
// calls Fetch on every SQLite request and compares the returned
|
|
// Page to the value held under key from the previous Fetch; when
|
|
// they match (the cache retained the entry) the same
|
|
// sqlite3_pcache_page stub is handed back to SQLite, and when they
|
|
// differ the binding retires the stale stub and mints a new one.
|
|
// The implementation is therefore free to evict and re-allocate
|
|
// between Fetches without leaking a stale view to SQLite.
|
|
Fetch(key uint32, mode FetchMode) Page
|
|
|
|
// Unpin tells the cache that the engine is finished using the
|
|
// Page for now. If discard is true SQLite has no further need
|
|
// for the page and the cache may release its memory; if discard
|
|
// is false the cache may retain the entry for reuse or release
|
|
// it at its discretion (the normal way to honour a bounded
|
|
// cache_size). SQLite never refcounts: one Unpin call is final
|
|
// regardless of how many Fetches preceded.
|
|
Unpin(p Page, discard bool)
|
|
|
|
// Rekey changes the key under which p is filed from oldKey to
|
|
// newKey. If an entry already exists at newKey it must be discarded
|
|
// in the same call; SQLite guarantees the colliding entry is not
|
|
// pinned at the moment of Rekey.
|
|
Rekey(p Page, oldKey, newKey uint32)
|
|
|
|
// Truncate discards every entry whose key is greater than or equal
|
|
// to limit, including pinned entries. This is the only callback
|
|
// permitted to evict a pinned page.
|
|
Truncate(limit uint32)
|
|
|
|
// Destroy releases every page and any resources owned by this
|
|
// Cache. After Destroy returns the binding will not call any
|
|
// other method on this Cache.
|
|
Destroy()
|
|
|
|
// Shrink hints the cache to release as much heap as possible. The
|
|
// implementation is not obligated to free anything; this is purely
|
|
// a memory-pressure advisory.
|
|
Shrink()
|
|
}
|
|
|
|
// Page is one cache entry. Buf and Extra return pointers into
|
|
// implementation-owned memory that MUST remain valid and at the same
|
|
// addresses for the duration of the pin: from the Fetch that returned
|
|
// the Page until the matching Unpin. While the page is unpinned the
|
|
// implementation is free to release the memory; the next Fetch for
|
|
// the same key will be consulted afresh and may return either the
|
|
// same Page (memory retained) or a different one.
|
|
//
|
|
// The memory MUST be off-heap: libc.Xmalloc, sqlite3_malloc, mmap, or
|
|
// an equivalent allocator outside the Go heap. Go-heap memory is
|
|
// forbidden, including memory pinned with runtime.Pinner: SQLite
|
|
// stores Extra addresses inside its own C structures and performs
|
|
// interior pointer arithmetic on them (it overlays PgHdr at the head
|
|
// of Extra), which trips Go's checkptr enforcement under -race the
|
|
// moment _sqlite3PcacheFetchFinish runs. Pinned slices preserve the
|
|
// allocation but lose checkptr provenance through the binding's
|
|
// unsafe.Pointer round-trip, so the failure surfaces only under the
|
|
// race detector and not in normal test runs.
|
|
//
|
|
// Page values are compared by the binding to detect whether the
|
|
// implementation retained or replaced the cached entry across a
|
|
// Fetch cycle, so Page MUST be a comparable type. Pointer-backed
|
|
// implementations (the natural shape) satisfy this automatically.
|
|
//
|
|
// Buf must be at least pageSize bytes and is where SQLite stores the
|
|
// database page contents. Extra must be at least extraSize bytes (the
|
|
// extraSize passed to PageCache.Create, which already includes
|
|
// SQLite's PgHdr overhead) and is treated by SQLite as opaque scratch
|
|
// space. Implementations should zero Extra on a freshly-allocated
|
|
// Page so SQLite's PgHdr backpointer is read as null; the binding
|
|
// does not touch Extra contents.
|
|
type Page interface {
|
|
Buf() unsafe.Pointer
|
|
Extra() unsafe.Pointer
|
|
}
|
|
|
|
// pcacheState holds the package-global state shared between
|
|
// RegisterPageCache and the Open path.
|
|
//
|
|
// Locking discipline:
|
|
//
|
|
// - openGate.RLock is held for the body of withOpenGate (called from
|
|
// the Driver.Open path). Many opens may proceed concurrently; what
|
|
// is forbidden is registering a page cache while any open is in
|
|
// flight.
|
|
// - openGate.Lock is held for the body of RegisterPageCache.
|
|
// The write lock drains all in-flight opens and blocks all
|
|
// subsequent opens until Xsqlite3_config completes.
|
|
// - opened is set with an unconditional Store on every Open. The
|
|
// Store is intentional and cheap; using CompareAndSwap to only
|
|
// mutate on the first Open buys nothing because the read-side
|
|
// under the write lock is uncontended. The atomic.Bool lets the
|
|
// hot Open path read under RLock without paying for a full mutex
|
|
// acquisition.
|
|
// - configOnce guarantees Xsqlite3_config runs at most once per
|
|
// process. A non-OK return code, an OOM during the methods-table
|
|
// allocation, or a panic inside the once body leaves configErr
|
|
// set and registered nil; every subsequent Register call returns
|
|
// the sticky configErr. Reload is not supported in this MR.
|
|
// - registered holds the canonical PageCache for idempotency
|
|
// comparison; it is non-nil only after a successful install.
|
|
// - cMethods is the libc.Xcalloc-owned C struct SQLite reads at
|
|
// sqlite3_initialize time. Allocated once and lives until process
|
|
// exit; allocating via libc avoids tripping Go's checkptr when
|
|
// the transpiled C code reads the struct (the same reasoning that
|
|
// vtab.go:130-141 uses for sqlite3_module).
|
|
var pcacheState struct {
|
|
openGate sync.RWMutex
|
|
opened atomic.Bool
|
|
configOnce sync.Once
|
|
configErr error
|
|
registered PageCache
|
|
cMethods uintptr
|
|
}
|
|
|
|
// markConnectionOpened is called from the Open path under
|
|
// pcacheState.openGate.RLock before sqlite3_open_v2. The RLock-side
|
|
// store happens-before RUnlock; RegisterPageCache's Lock
|
|
// acquisition waits for all readers to drain, so its subsequent Load
|
|
// observes every prior store.
|
|
func markConnectionOpened() {
|
|
pcacheState.opened.Store(true)
|
|
}
|
|
|
|
// withOpenGate runs fn while holding the openGate read lock. The Open
|
|
// path wraps its entire body in this so a concurrent
|
|
// RegisterPageCache cannot squeeze in between the opened-flag
|
|
// store and sqlite3_open_v2.
|
|
func withOpenGate(fn func() error) error {
|
|
pcacheState.openGate.RLock()
|
|
defer pcacheState.openGate.RUnlock()
|
|
markConnectionOpened()
|
|
return fn()
|
|
}
|
|
|
|
// pcacheMethods2 is the Go view of the C sqlite3_pcache_methods2 struct
|
|
// as cznic transpiles it. Every supported GOOS/GOARCH pair exports the
|
|
// type with the same FiVersion/FpArg/FxInit/... field names; the per-arch
|
|
// Go compiler emits the correct byte layout. The remaining old-generator
|
|
// arches (freebsd/386, freebsd/arm) are not in build_all_targets and do
|
|
// not currently build for unrelated upstream reasons, so no shim is needed.
|
|
type pcacheMethods2 = sqlite3.Tsqlite3_pcache_methods2
|
|
|
|
// RegisterPageCache installs m as the process-global SQLite page
|
|
// cache via SQLITE_CONFIG_PCACHE2. It MUST be called before the first
|
|
// sql.Open or driver.Open in the program.
|
|
//
|
|
// Concurrency contract:
|
|
//
|
|
// - Safe to call concurrently with itself and with other Register*
|
|
// entry points.
|
|
// - Blocks until any sql.Open calls currently in progress complete.
|
|
// Trade-off: a Register call may block for the duration of an
|
|
// in-flight Open. WAL recovery or cold-file-lock contention can
|
|
// make that wait visible.
|
|
// - Once any connection has been opened, returns ErrPageCacheTooLate
|
|
// without mutating the global module slot.
|
|
// - Calling twice with the same module value is a no-op success.
|
|
// Calling twice with a different value returns ErrPageCacheConflict.
|
|
// - A failed first install is sticky: every subsequent Register call
|
|
// returns the same error. Mutating the module fields after the
|
|
// first successful Register is silently ignored because SQLite has
|
|
// already copied the C methods table.
|
|
func RegisterPageCache(m PageCache) error {
|
|
if m == nil {
|
|
return errors.New("sqlite: RegisterPageCache(nil)")
|
|
}
|
|
|
|
pcacheState.openGate.Lock()
|
|
defer pcacheState.openGate.Unlock()
|
|
|
|
// Idempotency / conflict / too-late checks before we touch the
|
|
// once. pcacheState.registered is non-nil only after a SUCCESSFUL
|
|
// install, so observing it here means a prior Register completed
|
|
// without returning configErr.
|
|
if pcacheState.registered != nil {
|
|
if pcacheState.registered == m {
|
|
return nil
|
|
}
|
|
if pcacheState.opened.Load() {
|
|
return ErrPageCacheTooLate
|
|
}
|
|
return ErrPageCacheConflict
|
|
}
|
|
if pcacheState.opened.Load() {
|
|
return ErrPageCacheTooLate
|
|
}
|
|
|
|
// First-time install. The once body commits
|
|
// pcacheState.registered = m only after Xsqlite3_config succeeds.
|
|
// On OOM, SQLite error, or panic, configErr is set and registered
|
|
// stays nil; every subsequent Register returns configErr because
|
|
// the once is already fired.
|
|
pcacheState.configOnce.Do(func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
pcacheState.configErr = fmt.Errorf(
|
|
"sqlite: panic during PCACHE2 install: %v", r)
|
|
pcacheState.registered = nil
|
|
pcacheState.cMethods = 0
|
|
}
|
|
}()
|
|
|
|
tls := libc.NewTLS()
|
|
defer tls.Close()
|
|
|
|
methodsPtr := libc.Xcalloc(tls, 1, types.Size_t(unsafe.Sizeof(pcacheMethods2{})))
|
|
if methodsPtr == 0 {
|
|
pcacheState.configErr = errors.New("sqlite: out of memory allocating pcache_methods2")
|
|
return
|
|
}
|
|
populateCMethods(methodsPtr)
|
|
|
|
varArgs := libc.Xmalloc(tls, types.Size_t(unsafe.Sizeof(uintptr(0))))
|
|
if varArgs == 0 {
|
|
libc.Xfree(tls, methodsPtr)
|
|
pcacheState.configErr = errors.New("sqlite: out of memory allocating va_list")
|
|
return
|
|
}
|
|
defer libc.Xfree(tls, varArgs)
|
|
|
|
rc := sqlite3.Xsqlite3_config(tls,
|
|
int32(sqlite3.SQLITE_CONFIG_PCACHE2),
|
|
libc.VaList(varArgs, methodsPtr))
|
|
if rc != sqlite3.SQLITE_OK {
|
|
libc.Xfree(tls, methodsPtr)
|
|
pcacheState.configErr = fmt.Errorf(
|
|
"sqlite: Xsqlite3_config(SQLITE_CONFIG_PCACHE2) returned %d", rc)
|
|
return
|
|
}
|
|
|
|
// Commit only after every fallible step succeeded.
|
|
pcacheState.cMethods = methodsPtr
|
|
pcacheState.registered = m
|
|
})
|
|
|
|
return pcacheState.configErr
|
|
}
|
|
|
|
// MustRegisterPageCache is like RegisterPageCache but
|
|
// panics on any error. Intended for init() use where a missing page
|
|
// cache is fatal. Mirrors the precedent set by
|
|
// MustRegisterDeterministicScalarFunction.
|
|
func MustRegisterPageCache(m PageCache) {
|
|
if err := RegisterPageCache(m); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// populateCMethods fills the libc-owned SQLite C methods table with the
|
|
// addresses of the package-internal trampolines defined in
|
|
// pagecache_trampolines.go. The user's PageCache is reached
|
|
// through pcacheState.registered, which is set after this call returns
|
|
// successfully. Using top-level trampolines avoids passing user
|
|
// function values to cFuncPointer, which is undefined for closures and
|
|
// method values.
|
|
func populateCMethods(ptr uintptr) {
|
|
dst := (*pcacheMethods2)(unsafe.Pointer(ptr))
|
|
dst.FiVersion = 1
|
|
dst.FxInit = cFuncPointer(pcacheTrampolineInit)
|
|
dst.FxShutdown = cFuncPointer(pcacheTrampolineShutdown)
|
|
dst.FxCreate = cFuncPointer(pcacheTrampolineCreate)
|
|
dst.FxCachesize = cFuncPointer(pcacheTrampolineCachesize)
|
|
dst.FxPagecount = cFuncPointer(pcacheTrampolinePagecount)
|
|
dst.FxFetch = cFuncPointer(pcacheTrampolineFetch)
|
|
dst.FxUnpin = cFuncPointer(pcacheTrampolineUnpin)
|
|
dst.FxRekey = cFuncPointer(pcacheTrampolineRekey)
|
|
dst.FxTruncate = cFuncPointer(pcacheTrampolineTruncate)
|
|
dst.FxDestroy = cFuncPointer(pcacheTrampolineDestroy)
|
|
dst.FxShrink = cFuncPointer(pcacheTrampolineShrink)
|
|
}
|