Onboarding for AI agents and humans developing this repository. Read it top-to-bottom to be useful within ~5 minutes. (Agents using the package as a dependency want skills/ instead; end users want README.md and docs/.)
This file is canonical; CLAUDE.md is a pointer to it. Deep C-ABI internals live in dev/architecture.md.
gosqlite.org is a CGo-free SQLite driver for Go, a drop-in replacement for:
github.com/mattn/go-sqlite3— the C-bound driver; we register as"sqlite3".modernc.org/sqlite— the upstream CGo-free driver this fork builds on; we register as"sqlite".github.com/glebarez/sqliteandgorm.io/driver/sqlite— gorm dialectors; ours is thegorm/sub-package.
Plus first-class typed Go APIs for sqlite-vec vector search and FTS5 full-text search, encryption-at-rest, a user-implementable VFS, a bounded page cache, and a catalog of loadable Go SQL extensions.
Supported Go: the two most recent releases (the pin lives in go.mod; don't name versions in prose). Modern syntax is a feature — generics, iter.Seq2, log/slog, generic type aliases, range-over-int, sync.WaitGroup.Go, strings.SplitSeq, reflect.TypeFor are all in use. just lint runs gopls modernize to enforce the policy.
We fork modernc's hand-written Go wrapper (the root *.go files) so we can add per-conn methods and own the Driver type; the transpiled C (lib/, vec/, vfs/c/) stays an untouched external dependency. The wrapper talks to that C through a uintptr/unsafe.Pointer function-pointer dance centralized in internal/cabi. The weird-looking patterns (uintptr arithmetic, empty mutex critical sections, named-field struct literals) are the contract, not bugs — don't restyle them. Full rationale, the cabi primitives, and the struct-drift discipline: dev/architecture.md.
The high-signal map is the root-package fork surface; each sub-package documents its contract in a doc.go.
Root (modernc-derived + our additions):
sqlite.go init(), driver registration ("sqlite" + "sqlite3")
driver.go *Driver with Extensions / ConnectHook; dispatches remote-scheme DSNs
remote.go RegisterRemoteScheme — teaches the "sqlite" driver to open network DSNs (sql.Open("sqlite","quicsql://…")); the quicSQL seam, no network dep in the root
conn.go *conn (alias *Conn); most of the work happens here
stmt.go, rows.go, result.go, tx.go
error.go *Error with Code() / ExtendedCode()
convert.go SQLite ↔ Go value coercion
backup.go / backup_factory.go *Backup + (*Conn).Backup + Serialize/Deserialize
blob.go *Blob + (*Conn).OpenBlob — incremental BLOB I/O
extension.go LoadExtension / EnableLoadExtension
limits.go GetLimit / SetLimit
hooks.go Update / Authorizer / Trace hooks
rtree.go (*Conn).RegisterRTreeGeometry / RegisterRTreeQuery
session.go SESSION ext — CreateSession/ApplyChangeset/Invert/Concat + *Session
pre_update_hook.go RegisterPreUpdateHook / Commit / Rollback
fcntl.go file-control helpers (incl. EnableChecksums)
wal.go WALCheckpoint / WALAutoCheckpoint / RegisterWALHook
snapshot.go GetSnapshot / OpenSnapshot / SnapshotRecover + *Snapshot
control.go SetProgressHandler / SetDBConfig / QueryDBConfig
vtab.go / module.go virtual-table trampolines + CreateModule / CreateModuleSplit
pointer.go sqlite.Pointer for binding Go values into SQL params
mutex.go unlock_notify mutex wrapper
dsn.go mattn `_*` DSN-flag translator
constants.go SQLITE_* re-exports + ErrNo / ErrNoExtended
compat_*.go type aliases + reflective RegisterFunc / RegisterAggregator + conversion
stmt_cache.go per-conn prepared-stmt LRU + StmtCacheStats
introspect.go TableColumnMetadata / Status / TxnState + (*Stmt).Readonly / Status
config.go / open_config.go sqlite.Config / Pragmas / Encryption / TxLock + sqlite.Open(Config)
doc.go package doc (pkg.go.dev landing)
Sub-packages (each has a doc.go):
gorm/— gorm dialector, a separate module (gosqlite.org/gorm, its owngo.mod); the core module does not depend ongorm.io/gorm. Originally from glebarez; diverged: RETURNING always-on, OpenConfig, error translator.DropTableHook(a public extension point for third-party plugins) is defined here. Examples undergorm/examples/.vec/— sqlite-vec typedTable.fts/— FTS5 typedIndex[K, V].fusion/— RRF / RRF2 rank-fusion helpers (pure Go, no SQLite dep).sqlitex/— ergonomicdatabase/sqlhelpers (Save, Transaction, ExecScript, Execute, Result*, Migrate) in the zombiezen/crawshaw lineage.blobstore/— large, growable, randomly-writable byte objects over refcounted copy-on-write blocks behind a chunk mapping (io.ReaderAt/io.WriterAt, sparse holes, truncate, cheap clones, copy-on-write versions with retention, optional dedup, read-only open); built onConn.OpenBlob, conn-per-op. A separate module (gosqlite.org/blobstore, its owngo.mod) so its codec dependency stays out of the root graph; example underblobstore/example/. Coverage:dev/coverage/blobstore.md.vfs/—vfs.New(fs.FS)+vfs.NewReader; the public user-implementable VFS (vfs.Registerwithvfs.VFS/vfs.File, optionalvfs.ShmFilefor WAL,vfs.NoLockor thevfs.AdvisoryLockhelper for in-process file locking,vfs.Wrapinstrumentation; dispatcher inregister.go/iomethods.go/shm.go);vfs/cksm(page checksums),vfs/mvcc(snapshot-isolation in-memory),vfs/memdb(plain in-memory).vfs/crypto(encryption +crypto.Open) is a separate module (gosqlite.org/vfs/crypto, its owngo.mod+replace gosqlite.org => ../..) so its adiantum / x/crypto deps stay out of the root graph; example undervfs/crypto/example/. The root package no longer imports it — encryption is opened viacrypto.Open, and the rootConfig.VFSCloserseam lets any VFS module bundle teardown intodb.Close().vfs/vault/— a SQLite database in a block-structured container at rest where compression and encryption are independent options (plain, compressed, encrypted, or both):vault.Openis a live, page-translatingvfs.VFS(durable per transaction, multi-connection, WAL-capable) andvault.OpenSnapshotis the snapshot model (plaintext working copy, no-encryption case); plusPack/Unpack. Encryption is single-key (Options.Key) or multi-recipient keyslot (Options.Recipients/Identities, admins viaOptions.Masters/SignWith), with crash-safeRekey/Rewrap; tamper-evidence comes two ways — symmetric (Options.Authenticate, an HMAC root keyed by the data key, vs an attacker without the key) or writer-signed (Options.Writers/WriteAs, ed25519, for read-only recipients);Options.Anchor(aReplayAnchorkept outside the file — a TPM/keystore counter, orFileAnchor) upgrades that to rollback-RESISTANT (open rejects a generation below the recorded floor withErrRolledBack).Compactis the offline space-reclaim: it rewrites a closed container densely (returning freed blocks to the OS) while continuing the generation so an anchor stays valid. The at-rest magic isVAULTv01. A separate module (gosqlite.org/vfs/vault, its owngo.mod+replace gosqlite.org => ../..) so its codec/crypto deps stay out of the root graph; example undervfs/vault/example/. Coverage:dev/coverage/vault.md. Consolidates compression + encryption into one container.pcache/— application-controlled page cache (InstallBoundedLRUoverSQLITE_CONFIG_PCACHE2, off-heap blocks + the 11 PCACHE2 trampolines viainternal/cabi).internal/—cabi/(the C-ABI primitives),sqlid/(SQL-identifier toolkit),obs/(slog level-dispatch),raceskip/,testhelp/.ext/— loadable Go extensions, one sub-package per ext, each with anauto/blank-import. Inventory + status:dev/coverage/ext.md.ext/internal/filevtab/holds the file-vtab scaffolding shared byext/csv+ext/lines.tests/sql/— SQL conformance suite, organized by SQLite Language Reference category.examples/— runnable examples grouped by reader intent:migrating/,getting-started/,features/{search,vfs,extensions,advanced}/, plus standalone cross-module demos (liteorm/,encrypted-blobstore/,vault-blobstore/).examples/README.mdis the router. Smoke-tested byjust examples; run one withjust example <leaf-or-subpath>. (gorm examples live in thegorm/module undergorm/examples/.)
Dot-prefixed top-level dirs (e.g. .plans/) are local-only working state, gitignored; nothing in the module references them by name.
- libc version pin.
modernc.org/sqlite/libis transpiled C tied to a specificmodernc.org/libcversion — bumping one without the other breaks the ABI. Usejust bump-modernc vX.Y.Z(libc follows viago mod tidy); inspect withjust libc-pin. The single most likely source of "behaves erratically after a bump." - Two driver names, one singleton.
sql.Register("sqlite", drv)andsql.Register("sqlite3", drv)register the same*Driver;RegisterFunction/RegisterConnectionHookonce affects both. Never register two separate instances under the two names. - The C-ABI boundary (uintptr↔unsafe.Pointer,
internal/cabi, named-field struct literals, the bump-time field-list recheck) — seedev/architecture.md. Don't restyle the casts; re-check the struct field lists by hand on every modernc bump. - database/sql pool semantics. Hooks (Update/Authorizer/Trace/Commit/Rollback/PreUpdate) are per-connection;
db.Exec/db.Querymay pick any pooled conn. Tests installing a hook must pin the pool —internal/testhelp.OpenPinned(t, dsn)+testhelp.RawConn(sc, fn)is the canonical fixture. - sqlite-vec quirks.
INSERT OR REPLACEis not honored by vec0 (use(*Table).Update); vec0's column parser rejects quoted identifiers (we validate viavalidIdent);LIMIT/kmust be inlined as a literal (the planner needs it visible alongside MATCH); metric keywords arel1/l2/cosine(Dotaliases L1);modernc.org/sqlite/vecisn't transpiled for every GOOS/GOARCH (CI toleratesvec/build failures). - SQLite version is whatever
modernc.org/sqliteships — we don't pin or fork SQLite itself. - Userauth is dropped upstream; we reject
_auth*DSN flags with a clear error. Don't reintroduce it.
- Lint directives — two flavors.
staticcheckhonors//lint:ignore;golangci-linthonors//nolint:staticcheck. Where both are needed, use both. - errcheck path-scoped suppression.
.golangci.ymldisables errcheck for the modernc-derived files (conn|driver|stmt|rows|tx|backup|backup_factory|sqlite|vtab|pre_update_hook|fcntl), thegorm/port, tests, and examples. New code in new files is fully checked — don't smuggle new logic into an excluded file to dodge errcheck. interface{}isany. Always.- Test fixtures.
internal/testhelp.OpenPinned+RawConnis the canonical pinned-conn helper; per-sub-package fixtures (vec/table_test.go::openDB,fts/fts_test.go::openDB,vfs/crypto/crypto_test.go::freshKey,tests/sql/helper_test.go::openDB, …) handle domain seeding. Reuse them. - Comments: WHY not WHAT. A well-named identifier already says what; comments explain the non-obvious choice, the invariant preserved, or the upstream contract honored.
- Markdown: never hard-wrap prose (single long lines per paragraph; the renderer decides width). No version numbers in prose. No "Recent additions" / "Unreleased" holding sections — new rows go into their feature-section home.
| Task | Command |
|---|---|
| Build / test / lint | just build · just test · just lint |
| One named test | just test-one TestBLOB_ |
| Race detector | just test-race |
| Format check / apply | just fmt-check / just fmt |
| Run / smoke-test examples | just example <name> / just examples |
| Cross-build CI targets | just cross-build |
| Full CI locally | just ci |
| Benchmarks | just bench |
| Bump modernc / inspect libc pin | just bump-modernc vX.Y.Z / just libc-pin |
| List recipes | just --list |
just is convenience over vanilla go test ./..., not a build dependency.
First: does the typed API or the raw SQL path own this?
- Raw SQL / conn-level (DSN flag, hook,
Conn.Rawmethod) → root package; touches modernc-derived files, be conservative. - Vector →
vec/(typedTable). Full-text →fts/. gorm dialector/Migrator → the separategosqlite.org/gormmodule. Rank fusion →fusion/(pure Go). ORM-level vector/FTS search lives in the liteorm project, not here. - Encryption / VFS →
vfs/crypto/or the publicvfs/interface; do not patch the transpilation pipeline; honor the struct-drift discipline. - Loadable extension →
ext/<name>/with aRegister(*Conn) error+ a siblingext/<name>/auto/blank-import; track status indev/coverage/ext.md. - SQL conformance tests →
tests/sql/. Observability →Wrap(...)decorators (the per-packageRecordershape difference is intentional).
Always:
- Add tests in the package's
*_test.go(prefer integration tests over the public API). Runjust lintandjust testbefore reporting done. If you touched modernc-derived files, confirm the non-modernc packages still build and pass. - Don't quote test counts in user-facing docs — describe behavior, not numbers.
- Update every doc the change touches, in the same change. Doc drift is the #1 failure mode here. The set:
doc.gofor the package whose API moved (pkg.go.dev surface).README.mdonly if the change affects the landing page (feature bullet, comparison table, overview table) — keep README lean; deep content lives indocs/.docs/<section>/<page>.md— the user-facing guide/reference/extension page for the feature (this is where most narrative belongs now).skills/<name>/SKILL.md— the agent-usage recipe. Skills ship to consumers and go stale silently; treat updating them as part of the feature, not optional. Add a new skill folder when a feature is a distinct task an agent would do.dev/coverage/<area>.mdfor any vec / fts / gorm / vfs / ext / raw-SQL surface change (status flips, new test pins).- Never link
ARTICLE-EN.md/ARTICLE-RU.mdfrom any onboarding/consumer doc; never touchARTICLE-RU.md.
modernc.org/sqlite→just bump-modernc vX.Y.Z, thenjust test+just cross-build; the libc pin follows viago mod tidy. Re-check the struct field lists (dev/architecture.md).gorm.io/gorm→go getdirectly, thengo test ./gorm/...; major bumps occasionally needDialector.Initializetweaks (we gateRETURNINGon the SQLite feature-introduction version ingorm/sqlite.go).- Anything else (standalone
modernc.org/libc,golang.org/x/sys) — don't bump independently if implied by a modernc upgrade; the libc pin is the most fragile part of the graph.
| Question | File |
|---|---|
| DSN flag translation | dsn.go::translateMattnDSN |
RegisterFunc / RegisterAggregator |
compat_register.go + compat_convert.go |
| C→Go callback trampolines | hooks.go, pre_update_hook.go, vtab.go |
| The Go↔C function-pointer dance | internal/cabi/funcptr.go (FuncPointer / AsFunc) + callx.go; token/pointer maps in registry.go / ptrmap.go |
| Encryption-at-rest / corruption detection | vfs/crypto/ / vfs/cksm/ (+ their doc.go); (*Conn).EnableChecksums in fcntl.go |
| Incremental BLOB I/O | blob.go::OpenBlob + *Blob |
| WAL / snapshots / progress / db_config | wal.go, snapshot.go, control.go |
| Changesets (SESSION) | session.go — CreateSession → *Session, ApplyChangeset, InvertChangeset, ConcatChangesets. Example: examples/features/advanced/session |
| Custom R-Tree geometry/query | rtree.go (RegisterRTreeGeometry/RegisterRTreeQuery); ext/rtree ships a circle geometry |
| Stmt introspection (Column*/Bind*) | stmt.go::ColumnCount (for ext/statement + ext/pivot) |
Honor sqlite3_interrupt mid-vtab-loop |
(*Conn).IsInterrupted() in conn.go (used by ext/closure/spellfix1/pivot) |
| Stmt-cache telemetry | (*Conn).StmtCacheStats() in stmt_cache.go |
| Column metadata / runtime stats / txn state | introspect.go + (*Stmt).Readonly/Status in stmt.go |
| cksm/crypto chaining | vfs/crypto/crypto.go::Options.WrapVFS + per-package fileMap (cabi.PtrMap[FS]) |
| vtab xCreate/xConnect split | module.go::CreateModuleSplit (used by ext/bloom, ext/spellfix1) |
| vtab authoring / query-plan helpers | vtab.go::VTabDistinct (+ bestIndexRawCtx stash) · module.go::(*Conn).OverloadFunction · stmt.go::(*Stmt).Explain/IsExplain; backlog for the rest in .plans/plan-gosqlite-feature-backlog.md |
| vtab ctor runs on the EXECUTING conn | vtab.go::vtabInstantiate resolves the conn from the trampoline's db via connForDB, not the one captured at registration — else a per-conn-registered ctor's declare_vtab hits the wrong handle → SQLITE_MISUSE. Pinned by vtab_ctor_conn_test.go |
| Remote / network-backend dispatch | remote.go::RegisterRemoteScheme + driver.go::(*Driver).Open (remoteOpenerFor) — the seam the quicSQL forwarding driver plugs into; the root gains no network dependency |
| Shared SQL-identifier toolkit | internal/sqlid/sqlid.go |
| gorm Dialector / AutoMigrate | gorm/sqlite.go::Dialector / gorm/migrator.go::recreateTable |
| Embedding serialization / FTS SQL | vec/encoding.go / fts/fts.go::buildSearchSQL |
| vtab calling back into SQL on its host conn | vtab_nested_prepare_test.go pins it; ext/closure/pivot/statement use it |
| io.ReaderAt VFS / in-memory VFSes | vfs/vfs.go::NewReader / vfs/mvcc/ / vfs/memdb/ |
If you find yourself "fixing" any of these, stop and re-read — each is a deliberate, already-debated choice:
mutex.Lock(); mutex.Unlock()with no body (conn.go) — the documentedsqlite3_unlock_notifyhandshake.driver.Execer/driver.Queryermarked deprecated — we implement both deprecated and Context variants sodatabase/sqlfinds them on any Go version. Keep both.- nil collapse on empty BLOB reads — modernc returns
nilforbytes==0; we surface alen==0[]byte(blob_test.go::TestBLOB_EmptyBLOB). - No
INSERT OR REPLACEinvec.Insert— vec0 rejects it; useUpdate.vec.KNNinlines LIMIT as a literal — the planner needs it. TestLoadExtension_*skipped under-race/ darwin / windows — modernc's_sqlite3LoadExtensionpointer arithmetic trips checkptr; libc's dlopen/LoadLibraryW shims abort with "TODOTODO" before our error path. Opt-outs viainternal/raceskip+platform_test.go. linux is fine.- CI
build_all_targetsswallows avec/build failure —modernc.org/sqlite/vecisn't transpiled for every arch; the fallback lets us catch real regressions in the rest of the module.
Living tables under dev/coverage/: gorm.md, vec.md, fts.md, sql.md, ext.md, vfs.md, conn.md, session.md — each records status (✓/⚠/✗) and the test that exercises it. Upstream-suite reproduction recipes: dev/upstream/. Each file has a "last reviewed" footer; re-walk the affected matrix when you bump a dep. The ⚠ inherited cells are honest gaps — flipping one to ✓ is natural next-step work.
When in doubt, find an existing parallel feature and mirror it. The Observable wrappers (vec/observability.go / fts/observability.go) and the compat_*.go shim layer are the canonical templates.