gosuda.org/sqlite3 : pilote database/sql avec CGO_ENABLED=0 reposant sur l'IPC par mémoire partagée

gosuda.org/sqlite3 est un pilote database/sql pour Go avec CGO_ENABLED=0 qui exécute l'amalgamation C SQLite 3.54.1 en amont dans un processus de travail isolé, via des anneaux de mémoire partagée HQQ. Il s'enregistre sous le nom de pilote standard "sqlite3", prend en charge les paramètres de requête DSN de github.com/mattn/go-sqlite3, embarque des binaires serveur préconstruits et attestés SLSA pour six cibles de systèmes d'exploitation et d'architectures, et utilise un pipelining des requêtes avec un préchargement par lots de 64 KiB afin de surpasser le cgo en processus sur les requêtes renvoyant plusieurs lignes.
1go get gosuda.org/sqlite3@v0.3.3540100
Pourquoi un Autre Pilote SQLite pour Go ?
SQLite est l'un des logiciels les plus déployés au monde, pourtant son utilisation depuis Go a historiquement contraint les ingénieurs à un choix inconfortable entre simplicité de construction et fidélité au moteur.
D'un côté se trouve github.com/mattn/go-sqlite3, référence communautaire de longue date. Il encapsule directement l'amalgamation C officielle de SQLite via cgo. On obtient ainsi le véritable planificateur de requêtes C, la sémantique complète de concurrence WAL et une gestion mémoire C prévisible. Le coût réside dans CGO_ENABLED=1 : les compilations locales et les pipelines CI ralentissent, les images de conteneurs minimales (scratch ou distroless) exigent des bibliothèques d'exécution C correspondantes, la compilation croisée vers un autre système d'exploitation ou une autre architecture processeur nécessite une chaîne d'outils C croisée complète, et toute corruption mémoire en C partage l'espace d'adressage du processus Go.
De l'autre côté se trouvent les portages purs Go de SQLite, générés par une traduction mécanique de l'amalgamation C en code source Go, sur un runtime C émulé (libc). Ils rétablissent CGO_ENABLED=0, ce qui rend la compilation triviale. Sous des charges soutenues, cependant, le tas C traduit et les allocations Go par ligne augmentent la pression sur le ramasse-miettes, et des cas limites subtils liés au verrouillage, aux délais d'attente occupés (busy timeouts) ou à l'interruption d'instructions peuvent diverger des compilations C natives.
Nous avons conçu gosuda.org/sqlite3 afin que les applications Go n'aient pas à choisir entre CGO_ENABLED=0 et le moteur C SQLite réel. En compilant l'amalgamation C officielle de SQLite 3.54.1 dans un serveur dédié à la mémoire partagée (hqq-sqlite-server) et en communiquant via gosuda.org/hqq, votre binaire Go se compile avec CGO_ENABLED=0 tout en exécutant un SQLite C non modifié, optimisé avec -O3, à travers une frontière de processus.
Exécution sans Configuration : De go get à sql.Open
Remplacer github.com/mattn/go-sqlite3 dans une base de code Go existante ne demande que de modifier le chemin d'importation anonyme. Le pilote s'enregistre sous le nom "sqlite3" (et "hqq-sqlite" pour une sélection explicite) et accepte les mêmes chaînes de requête 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}
Binaires de Travail Embarqués et Attestés SLSA
Une question naturelle lorsqu'on apprend qu'une bibliothèque utilise un processus de travail C est : « Dois-je installer ou livrer un binaire C séparé avec mon programme Go ? »
Non. gosuda.org/sqlite3 embarque des binaires hqq-sqlite-server préconstruits pour les six cibles principales, directement dans des fichiers Go marqués par plateforme (internal/embedded/embed_<goos>_<goarch>.go). Comme les contraintes de compilation de Go n'incluent que le fichier correspondant à vos GOOS et GOARCH cibles, une compilation croisée GOOS=linux GOARCH=arm64 go build n'embarque automatiquement que le binaire serveur linux/arm64 d'environ 1,4 MiB.
| Système cible | Architecture cible (GOARCH) | Binaire embarqué | Chaîne de compilation et provenance |
|---|---|---|---|
| Linux | amd64, arm64 | hqq-sqlite-server_linux_<arch> | GCC -O3, provenance SLSA Build L3 |
| macOS | arm64, amd64 | hqq-sqlite-server_darwin_<arch> | AppleClang -O3, provenance SLSA Build L3 |
| Windows | amd64 | hqq-sqlite-server_windows_amd64.exe | MSVC -O2, provenance SLSA Build L3 |
| Windows | arm64 | hqq-sqlite-server_windows_arm64.exe | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Lorsque sql.Open établit sa première connexion, autolaunch.go matérialise le binaire embarqué dans le répertoire de cache utilisateur ($XDG_CACHE_HOME/hqq/bin/<sha256>/), vérifie son empreinte cryptographique SHA-256 par rapport à la table ReleaseHashes épinglée dans hashes.go via gosuda.org/hqq/secureexec, puis lance le travailleur avec --parent-stdin afin que sa durée de vie soit strictement liée à votre processus Go.
Compatibilité DSN Clean-Room avec mattn/go-sqlite3
Pour fonctionner comme un véritable remplacement direct, l'analyseur DSN (dsn.go) implémente une réplique clean-room des options de connexion et des pragmas utilisés dans les bases de code mattn/go-sqlite3 :
| Paramètre de requête DSN | Valeurs acceptées | Effet SQLite |
|---|---|---|
_busy_timeout / _timeout | Entier en millisecondes (ex. 5000) | Appelle sqlite3_busy_timeout(db, ms) à l'ouverture de la session. |
_journal_mode / _journal | DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF | Exécute PRAGMA journal_mode = <mode>. |
_txlock | immediate, exclusive, deferred | Sélectionne BEGIN IMMEDIATE, BEGIN EXCLUSIVE ou BEGIN DEFERRED pour db.BeginTx. |
_foreign_keys / _fk | on, true, 1, off, false, 0 | Exécute PRAGMA foreign_keys = ON/OFF. |
_synchronous / _sync | 0/OFF, 1/NORMAL, 2/FULL, 3/EXTRA | Exécute PRAGMA synchronous = <niveau>. |
_cache_size | Entier en pages ou -KiB (ex. -20000) | Exécute PRAGMA cache_size = <val>. |
_loc | auto ou fuseau horaire IANA (ex. UTC, Asia/Seoul) | Analyse les colonnes texte DATETIME/TIMESTAMP en *time.Time dans le fuseau cible. |
mode | ro, rw, rwc, memory | Définit SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE ou SQLITE_OPEN_MEMORY. |
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuum | Booléen ou entiers de mode | Applique le PRAGMA de session correspondant lors de l'initialisation de la connexion. |
Transformer l'Isolation des Processus en Avantage de Débit
À première vue, exécuter SQLite dans un processus séparé semble forcément plus lent que d'appeler sqlite3_step en processus via cgo. En pratique, c'est l'inverse dès qu'une requête renvoie plus d'une ligne.
Le Coût Caché des Appels cgo Ligne par Ligne
Dans un pilote SQLite classique basé sur cgo, database/sql pilote l'itération une ligne et une colonne à la fois. La lecture de 1 000 lignes à 4 colonnes déclenche :
- 1 000 appels
cgoàsqlite3_step - 4 000 appels
cgoàsqlite3_column_typeetsqlite3_column_int64/sqlite3_column_text - Des milliers de franchissements de frontière individuels entre l'ordonnanceur Go et la pile C
Pipelining des Requêtes et Préchargement par Lots de 64 KiB
gosuda.org/sqlite3 élimine les franchissements de frontière par ligne en repensant le protocole filaire autour de deux mécanismes situés dans stmt.go, rows.go et sqlite3/server/sqlite3_hooks.c :
1Client Go (stmt.go / rows.go) Travailleur C11 (sqlite3_hooks.c)
2----------------------------- ---------------------------------
31. Pipeline en 1 envoi :
4 ResetAsync + BindValuesAsync
5 + StmtInfoAsync + FetchRowsAsync(128) =======> reset -> bind -> stmt_info ->
6 boucle sqlite3_step() jusqu'à 128 lignes
7 <======= Lot 1 (lignes 1..128 empaquetées dans un slot de 64 KiB)
82. Déclenchement immédiat du préchargement :
9 FetchRowsAsync(128) (Lot 2) =======> boucle sqlite3_step() pour les lignes 129..256
10 Pendant que C exécute le Lot 2, l'appelant
11 Go parcourt les lignes 1..128 du Lot 1 ! <======= Lot 2 prêt en mémoire partagée
- Pipelining de requête en vol unique (
stmt.go) : LorsqueQueryContextouStmt.QueryContextexécute une instruction en lecture seule,s.query()soumet de manière asynchronereset,bind_values,stmt_info(à la première exécution) etfetch_rowsdans un seul envoi en mémoire partagée. Les quatre opérations s'exécutent successivement sur le travailleur C et reviennent en un seul aller-retour. - Boucles de lots C serrées et préchargement asynchrone (
rows.go) : Dansfetch_rows(sqlite3_hooks.c), le travailleur C exécutesqlite3_step(stmt)dans une boucle C native, encodant jusqu'àFetchSizelignes (par défaut 128) directement dans le slot de réponse de 64 KiB en mémoire partagée, jusqu'à atteindre la limite du lot, le remplissage du slot ouSQLITE_DONE. Dès querows.ingest()décode le Lot 1, il déclencheFetchRowsAsyncpour le Lot 2 avant de renvoyer la première ligne à l'appelant. Pendant que votre code Go parcourt le Lot 1, le travailleur C exécute déjà le Lot 2 sur un autre cœur de processeur.
Sur Apple M4 (darwin/arm64), la comparaison entre gosuda.org/sqlite3 (CGO_ENABLED=0) et github.com/mattn/go-sqlite3 (CGO_ENABLED=1) sur la même base de données SQLite montre l'effet du regroupement par lots et du pipelining :
| Charge de travail | gosuda.org/sqlite3 (CGO_ENABLED=0) | mattn/go-sqlite3 (CGO_ENABLED=1) | Vitesse relative |
|---|---|---|---|
Sélection ponctuelle (QueryRow par PK) | 3,52 µs/op (9 B, 1 alloc) | 3,56 µs/op (232 B, 11 allocs) | 1,01x (parité, 11x moins d'allocs) |
Parcours de 100 lignes (rows.Next) | 20,41 µs/op (30,5 KB, 211 allocs) | 36,34 µs/op (21,8 KB, 309 allocs) | 1,78x plus rapide |
Parcours de 1 000 lignes (rows.Next) | 125,92 µs/op (296 KB, 2 030 allocs) | 349,10 µs/op (216 KB, 3 009 allocs) | 2,77x plus rapide |
| Insertion préparée (Autocommit) | 5,33 µs/op (0 B, 0 allocs) | 5,88 µs/op (120 B, 5 allocs) | 1,10x plus rapide (0 allocs) |
| Lot de transaction de 100 lignes | 189,99 µs/op (16 B, 1 alloc) | 191,50 µs/op (12,1 KB, 503 allocs) | 1,01x (500x moins d'allocs) |
Annulation, Cloisonnement des Chemins et Récupération du Pool de Connexions
L'exécution de SQLite dans un travailleur isolé renforce également la manière dont le pilote gère les délais d'attente, le SQL non fiable et les frontières du système de fichiers :
- Annulation de contexte déterministe (
conn.goeterrors.go) : Chaque appel au pilote arme une garde légèrecontext.AfterFunc(cn.guard). Si lecontext.Contextde l'appelant est annulé ou si son délai expire pendant qu'une requête récursive lente ou qu'une attente de verrou s'exécute sur le serveur, la garde marque la connexion comme défectueuse, ferme le lien de session local et envoie une trameCancel()hors bande. Le travailleur C invoque immédiatementsqlite3_interrupt(db), débloquantsqlite3_step, tandis que le pilote Go traduit le statutSQLITE_INTERRUPTrésultant encontext.Canceledoucontext.DeadlineExceededet demande àdatabase/sqlde remplacer la connexion dans le pool. - Cloisonnement des répertoires (
sqlsrv_pathrules.c) : Le travailleur C applique les règles de chemin--allow-dirà chaque appelopen_v2, en résolvant les liens symboliques et en rejetant les traversées relatives..hors du répertoire autorisé.sqlite3_load_extensionetSQLITE_LIMIT_ATTACHEDsont désactivés par défaut afin qu'une injection SQL ne puisse ni sortir du répertoire de la base de données ni charger d'objets partagés arbitraires.
Disposition du Dépôt
Le dépôt est structuré de sorte que le paquet de niveau supérieur fournisse le pilote database/sql directement utilisable, tandis que launcher/ et sqlite3/ restent accessibles pour les déploiements avancés de serveurs personnalisés :
1gosuda.org/sqlite3
2├── driver.go / api.go # Enregistrement du pilote database/sql ("sqlite3") et API Connector
3├── dsn.go # Analyseur DSN clean-room mattn/go-sqlite3 et configuration des pragmas
4├── conn.go / tx.go # Cycle de vie de session, garde de contexte et machine d'état des transactions
5├── stmt.go / rows.go # Pipelining de requêtes en vol unique et préchargement par lots de 64 KiB
6├── errors.go # Codes d'erreur SQLite primaires/étendus typés et correspondance de sentinelles
7├── autolaunch.go / hashes.go # Matérialisation du serveur embarqué sans configuration et épinglage SHA-256
8├── internal/embedded/ # Binaires //go:embed marqués par plateforme (linux, darwin, windows)
9├── launcher/ # Superviseur de processus configurable pour déploiements de serveurs personnalisés
10└── sqlite3/ # Client Go hqq-bindgen généré et code source du serveur C11 SQLite
11 └── server/ # Implémentation du serveur C11 (sqlite3_hooks.c, sqlsrv_policy.c)
Premiers Pas
Installez le module avec go get et ouvrez votre base de données avec sql.Open("sqlite3", dsn) :
1go get gosuda.org/sqlite3@v0.3.3540100
- Dépôt GitHub : https://github.com/gosuda/hqq-sqlite3
- Référence DSN et pragmas : docs/DSN.md
- Architecture et protocole filaire : docs/ARCHITECTURE.md