gosuda.org/sqlite3: A Drop-In database/sql Driver Running Native C SQLite Over Shared Memory
gosuda.org/sqlite3 je ovladač database/sql pro Go s nastavením CGO_ENABLED=0, který spouští upstreamový C SQLite 3.54.1 amalgam uvnitř izolovaného procesního workeru prostřednictvím kruhových vyrovnávacích pamětí se sdílenou pamětí HQQ. Registruje se pod standardním názvem ovladače "sqlite3", podporuje parametry dotazů DSN z knihovny github.com/mattn/go-sqlite3, vkládá předem sestavené binární soubory serveru ověřené pomocí SLSA pro šest cílových operačních systémů a architektur a využívá pipelining požadavků s dávkovým prefetchingem o velikosti 64 KiB, čímž překonává in-process cgo u dotazů vracejících více řádků.
1go get gosuda.org/sqlite3@v0.3.3540100
Proč další ovladač SQLite pro Go?
SQLite je jedním z nejrozšířenějších softwarových produktů na světě, přesto jeho používání z jazyka Go historicky nutilo inženýry k nepříjemné volbě mezi jednoduchostí sestavení a věrností enginu.
Na jedné straně stojí github.com/mattn/go-sqlite3, dlouhodobý komunitní standard. Přímo obaluje oficiální C amalgam SQLite pomocí cgo. Získáte skutečný C plánovač dotazů, úplnou sémantiku souběžnosti WAL a předvídatelnou správu paměti v jazyce C. Nákladem je CGO_ENABLED=1: lokální sestavení a CI pipeline se zpomalují, minimální scratch nebo distroless obrazové kontejnery vyžadují odpovídající knihovny C runtime, cross-kompilace pro jiný operační systém nebo CPU architekturu vyžaduje kompletní C cross-toolchain a jakékoli poškození paměti v jazyce C sdílí adresní prostor procesu Go.
Na druhé straně stojí čistě goovské implementace SQLite generované mechanickým převodem C amalgamu do zdrojového kódu Go nad emulovaným C runtime (libc). Obnovují CGO_ENABLED=0, což činí sestavení triviálním. Avšak při dlouhodobém zatížení zvyšují přeložená C halda a alokace v Go na jeden řádek tlak na garbage collector a jemné okrajové případy v zamykání, timeoutech při zaneprázdnění nebo přerušení příkazů se mohou lišit od nativních C sestavení.
Vytvořili jsme gosuda.org/sqlite3, aby aplikace v Go nemusely volit mezi CGO_ENABLED=0 a skutečným C enginem SQLite. Zkompilováním oficiálního C amalgamu SQLite 3.54.1 do vyhrazeného serveru se sdílenou pamětí (hqq-sqlite-server) a komunikací přes gosuda.org/sqlite3 se vaše binární soubory v Go zkompilují s CGO_ENABLED=0, zatímco vykonávají neupravený C SQLite optimalizovaný pomocí -O3 napříč hranicí procesu.
Bezkonfigurační spuštění: Od go get po sql.Open
Nahrazení github.com/mattn/go-sqlite3 v existující kódové základně Go vyžaduje změnu pouze prázdné cesty importu. Ovladač se registruje jako "sqlite3" (a "hqq-sqlite" pro explicitní výběr) a akceptuje stejné řetězce dotazů DSN:
1package main
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "log"
8
9 _ "gosuda.org/sqlite3"
10)
11
12func main() {
13 dsn := "file:app.db?_journal_mode=WAL&_busy_timeout=5000&_txlock=immediate&_foreign_keys=on"
14 db, err := sql.Open("sqlite3", dsn)
15 if err != nil {
16 log.Fatal(err)
17 }
18 defer db.Close()
19
20 ctx := context.Background()
21 if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`); err != nil {
22 log.Fatal(err)
23 }
24 if _, err := db.ExecContext(ctx, `INSERT INTO users (name) VALUES (?)`, "Ada Lovelace"); err != nil {
25 log.Fatal(err)
26 }
27
28 var id int64
29 var name string
30 if err := db.QueryRowContext(ctx, `SELECT id, name FROM users WHERE name = ?`, "Ada Lovelace").Scan(&id, &name); err != nil {
31 log.Fatal(err)
32 }
33 fmt.Printf("user #%d: %s\n", id, name)
34}
Vložené binární soubory workeru ověřené pomocí SLSA
Přirozenou otázkou při zjištění, že knihovna používá C proces workeru, je: "Musím instalovat nebo distribuovat samostatný C binární soubor společně s mým programem v Go?"
Ne. gosuda.org/sqlite3 vkládá předem sestavené binární soubory hqq-sqlite-server pro všech šest hlavních cílů přímo uvnitř souborů Go označených platformou (internal/embedded/embed_<goos>_<goarch>.go). Vzhledem k tomu, že sestavovací omezení jazyka Go kompilují pouze soubor odpovídající vašemu cílovému GOOS a GOARCH, cross-kompilace pomocí GOOS=linux GOARCH=arm64 go build automaticky vložení pouze binárního souboru serveru linux/arm64 o velikosti cca 1,4 MiB.
| Cílový OS | Cílová arch (GOARCH) | Vložený binární soubor | Sestavovací toolchain a provenience |
|---|---|---|---|
| Linux | amd64, arm64 | hqq-sqlite-server_linux_<arch> | GCC -O3, SLSA Build L3 Provenance |
| macOS | arm64, amd64 | hqq-sqlite-server_darwin_<arch> | AppleClang -O3, SLSA Build L3 Provenance |
| Windows | amd64 | hqq-sqlite-server_windows_amd64.exe | MSVC -O2, SLSA Build L3 Provenance |
| Windows | arm64 | hqq-sqlite-server_windows_arm64.exe | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Když sql.Open naváže své první připojení, soubor autolaunch.go materializuje vložený binární soubor do adresáře mezipaměti uživatele ($XDG_CACHE_HOME/hqq/bin/<sha256>/), ověří jeho kryptografický součet SHA-256 oproti připnuté mapě ReleaseHashes v souboru hashes.go prostřednictvím gosuda.org/hqq/secureexec a spustí worker s přepínačem --parent-stdin, takže jeho životnost je striktně vázána na váš proces Go.
Čistá kompatibilita DSN pro mattn/go-sqlite3
Aby fungoval jako skutečná náhrada typu drop-in, analyzátor DSN (dsn.go) implementuje čistou repliku připojení a pragma přepínačů používaných napříč kódovými základnami mattn/go-sqlite3:
| Parametr dotazu DSN | Akceptované hodnoty | Účinek v SQLite |
|---|---|---|
_busy_timeout / _timeout | Celá čísla v milisekundách (např. 5000) | Při otevření relace zavolá sqlite3_busy_timeout(db, ms). |
_journal_mode / _journal | DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF | Provede PRAGMA journal_mode = <mode>. |
_txlock | immediate, exclusive, deferred | Zvolí BEGIN IMMEDIATE, BEGIN EXCLUSIVE nebo BEGIN DEFERRED pro db.BeginTx. |
_foreign_keys / _fk | on, true, 1, off, false, 0 | Provede PRAGMA foreign_keys = ON/OFF. |
_synchronous / _sync | 0/OFF, 1/NORMAL, 2/FULL, 3/EXTRA | Provede PRAGMA synchronous = <level>. |
_cache_size | Celé číslo stránek nebo -KiB (např. -20000) | Provede PRAGMA cache_size = <val>. |
_loc | auto nebo časové pásmo IANA (např. UTC, Asia/Seoul) | Analyzuje textové sloupcové hodnoty DATETIME/TIMESTAMP do *time.Time v cílovém umístění. |
mode | ro, rw, rwc, memory | Nastaví SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE nebo SQLITE_OPEN_MEMORY. |
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuum | Booleanové hodnoty nebo celá čísla režimu | Aplikuje odpovídající relaci PRAGMA při inicializaci připojení. |
Přeměna izolace procesů v propustnostní výhodu
Na první pohled se zdá, že spuštění SQLite v samostatném procesu musí být pomalejší než volání sqlite3_step v rámci procesu pomocí cgo. V praxi dochází k opačnému jevu, jakmile dotaz vrací více než jediný řádek.
Skryté náklady na volání cgo řádek po řádku
V konvenčním ovladači SQLite založeném na cgo řídí database/sql iteraci po jednom řádku a jednom sloupci. Přečtení 1 000 řádků se 4 sloupci spouští:
- 1 000 volání
cgona funkcisqlite3_step - 4 000 volání
cgona funkcesqlite3_column_typeasqlite3_column_int64/sqlite3_column_text - Tisíce individuálních přechodů hranic mezi plánovačem Go a zásobníkem C
Pipelining požadavků a dávkový prefetching o velikosti 64 KiB
gosuda.org/sqlite3 eliminuje přechody hranic na úrovni jednotlivých řádků přepracováním síťového protokolu kolem dvou mechanismů v souborech stmt.go, rows.go a sqlite3/server/sqlite3_hooks.c:
1Go Client (stmt.go / rows.go) C11 Worker (sqlite3_hooks.c)
2----------------------------- ----------------------------
31. Pipeline in 1 flight:
4 ResetAsync + BindValuesAsync
5 + StmtInfoAsync + FetchRowsAsync(128) =======> reset -> bind -> stmt_info ->
6 loop sqlite3_step() up to 128 rows
7 <======= Batch 1 (rows 1..128 packed in 64 KiB slot)
82. Immediately fire prefetch:
9 FetchRowsAsync(128) (Batch 2) =======> loop sqlite3_step() for rows 129..256
10 While C steps Batch 2, Go caller
11 scans rows 1..128 from Batch 1! <======= Batch 2 ready in shared memory
- Jednosměrný pipelining dotazů (
stmt.go): KdyžQueryContextneboStmt.QueryContextspustí příkaz pouze pro čtení,s.query()odešle asynchronněreset,bind_values,stmt_info(při prvním spuštění) afetch_rowsv jediném přenosu sdílené paměti. Všechny čtyři operace se provedou bezprostředně za sebou na C workeru a vrátí se v jedné zpáteční cestě. - Úzké dávkové smyčky C a asynchronní prefetching (
rows.go): Uvnitřfetch_rows(sqlite3_hooks.c) provádí C worker krokysqlite3_step(stmt)v nativní smyčce jazyka C, přičemž kóduje ažFetchSizeřádků (výchozí hodnota 128) přímo do slotu odpovědi se sdílenou pamětí o velikosti 64 KiB, dokud není dosaženo limitu dávky, slot není plný nebo nenastaneSQLITE_DONE. Jakmilerows.ingest()dešifruje dávku Batch 1, spustíFetchRowsAsyncpro dávku Batch 2 před vrácením prvního řádku volajícímu. Zatímco váš kód v Go prohledává dávku Batch 1, C worker již zpracovává dávku Batch 2 na jiném jádře CPU.
Na Apple M4 (darwin/arm64) porovnání gosuda.org/sqlite3 (CGO_ENABLED=0) s github.com/mattn/go-sqlite3 (CGO_ENABLED=1) na stejné databázi SQLite ukazuje vliv dávkování a pipeliningu:
| Pracovní zátěž | gosuda.org/sqlite3 (CGO_ENABLED=0) | mattn/go-sqlite3 (CGO_ENABLED=1) | Relativní rychlost |
|---|---|---|---|
Bodový výběr (QueryRow podle PK) | 3.52 µs/op (9 B, 1 alokace) | 3.56 µs/op (232 B, 11 alokací) | 1,01x (parita, 11x méně alokací) |
Prohledání 100 řádků (rows.Next) | 20.41 µs/op (30,5 KB, 211 alokací) | 36.34 µs/op (21,8 KB, 309 alokací) | 1,78x rychlejší |
Prohledání 1 000 řádků (rows.Next) | 125.92 µs/op (296 KB, 2 030 alokací) | 349.10 µs/op (216 KB, 3 009 alokací) | 2,77x rychlejší |
| Připravený vklad (Autocommit) | 5.33 µs/op (0 B, 0 alokací) | 5.88 µs/op (120 B, 5 alokací) | 1,10x rychlejší (0 alokací) |
| Dávka transakce 100 řádků | 189.99 µs/op (16 B, 1 alokace) | 191.50 µs/op (12,1 KB, 503 alokací) | 1,01x (500x méně alokací) |
Zrušení, sandboxování cest a obnova fondu připojení
Spouštění SQLite v izolovaném workeru rovněž posiluje způsob, jakým ovladač zvládá timeouty, nedůvěryhodné SQL dotazy a hranice souborového systému:
- Deterministické zrušení kontextu (
conn.goaerrors.go): Každé volání ovladače aktivuje lehkou ochranucontext.AfterFunc(cn.guard). Pokud je kontextcontext.Contextvolajícího zrušen nebo vyprší jeho časový limit (deadline) během probíhajícího pomalého rekurzivního dotazu nebo čekání na zámek na serveru, ochrana označí připojení jako vadné, ukončí místní odkaz relace a odešle mimo-pásmový (out-of-band) rámecCancel(). C worker okamžitě vyvolásqlite3_interrupt(db), čímž odblokujesqlite3_step, zatímco ovladač Go mapuje výsledný stavSQLITE_INTERRUPTnacontext.Cancelednebocontext.DeadlineExceededa instruujedatabase/sqlk nahrazení připojení ve fondu. - Sandboxování adresářů (
sqlsrv_pathrules.c): C worker vynucuje pravidla cest--allow-dirpři každém voláníopen_v2, přičemž řeší symbolické odkazy a odmítá relativní průchod pomocí..mimo povolený adresář.sqlite3_load_extensionaSQLITE_LIMIT_ATTACHEDjsou ve výchozím nastavení zakázány, takže injektáž SQL nemůže uniknout z adresáře databáze ani načíst libovolné sdílené objekty.
Uspořádání úložiště
Úložiště je strukturováno tak, že balíček nejvyšší úrovně poskytuje ovladač database/sql typu drop-in, zatímco launcher/ a sqlite3/ zůstávají přístupné pro pokročilá nasazení vlastních serverů:
1gosuda.org/sqlite3
2├── driver.go / api.go # database/sql driver registration ("sqlite3") and Connector API
3├── dsn.go # Clean-room mattn/go-sqlite3 DSN parser and pragma configuration
4├── conn.go / tx.go # Session lifecycle, context guard, and transaction state machine
5├── stmt.go / rows.go # Single-flight request pipelining and 64 KiB batch prefetching
6├── errors.go # Typed SQLite primary/extended error codes and sentinel matching
7├── autolaunch.go / hashes.go # Zero-config embedded server materialization and SHA-256 pinning
8├── internal/embedded/ # Platform-tagged //go:embed binaries (linux, darwin, windows)
9├── launcher/ # Configurable process supervisor for custom server deployments
10└── sqlite3/ # Generated hqq-bindgen Go client and C11 SQLite server source
11 └── server/ # C11 server implementation (sqlite3_hooks.c, sqlsrv_policy.c)
Začínáme
Nainstalujte modul pomocí příkazu go get a otevřete databázi pomocí sql.Open("sqlite3", dsn):
1go get gosuda.org/sqlite3@v0.3.3540100
- Úložiště GitHub: https://github.com/gosuda/hqq-sqlite3
- Reference DSN a pragma: docs/DSN.md
- Architektura a síťový protokol: docs/ARCHITECTURE.md