gosuda.org/sqlite3: Sterownik database/sql stanowiący bezpośredni zamiennik, uruchamiający natywny C SQLite poprzez pamięć współdzieloną (Shared Memory)
gosuda.org/sqlite3 jest sterownikiem database/sql dla języka Go z ustawioną flagą CGO_ENABLED=0, który uruchamia główny kod źródłowy C SQLite w wersji 3.54.1 (ang. amalgamation) wewnątrz wyizolowanego procesu roboczego za pośrednictwem pierścieni pamięci współdzielonej HQQ. Rejestruje się on pod standardową nazwą sterownika "sqlite3", obsługuje parametry zapytań DSN biblioteki github.com/mattn/go-sqlite3, osadza wstępnie skompilowane pliki binarne serwera poświadczone mechanizmem SLSA dla sześciu docelowych systemów operacyjnych i architektur oraz wykorzystuje potokowanie żądań z prefetchingiem wsadowym o wielkości 64 KiB, aby prześcignąć wbudowane w proces wywołania cgo w zapytaniach dotyczących wielu wierszy.
1go get gosuda.org/sqlite3@v0.3.3540100
Dlaczego Kolejny Sterownik SQLite dla Języka Go?
SQLite jest jednym z najszerzej wdrożonych oprogramowań na świecie, a jednak korzystanie z niego w języku Go historycznie zmuszało inżynierów do podjęcia niewygodnego wyboru pomiędzy prostotą kompilacji a wiernością silnika.
Z jednej strony stoi github.com/mattn/go-sqlite3, od lat stanowiący standard społeczności. Opakowuje on oficjalny kod źródłowy C SQLite bezpośrednio za pomocą cgo. Otrzymujesz prawdziwy planista zapytań (ang. query planner) w języku C, pełną semantykę współbieżności WAL oraz przewidywalne zarządzanie pamięcią w języku C. Kosztem jest CGO_ENABLED=1: lokalne kompilacje oraz rurociągi CI zwalniają, minimalne obrazy kontenerów typu scratch lub distroless wymagają pasujących bibliotek środowiska wykonawczego C, skompilowanie skrośne na inny system operacyjny lub architekturę procesora wymaga pełnego łańcucha narzędzi skrośnych C, a wszelkie uszkodzenia pamięci w języku C współdzielą przestrzeń adresową procesu Go.
Z drugiej strony znajdują się porty SQLite napisane w czystym Go, wygenerowane poprzez mechaniczne przetłumaczenie kodu źródłowego C na kod źródłowy Go nad emulowanym środowiskiem wykonawczym C (libc). Przywracają one ustawienie CGO_ENABLED=0, co sprawia, że kompilacja jest trywialna. Jednak podtrzymywane obciążenia robocze sprawiają, że przetłumaczona giełda (ang. heap) języka C oraz alokacje Go przypadające na każdy wiersz zwiększają presję na odśmiacz pamięci (ang. garbage collection), a subtelne przypadki brzegowe dotyczące blokowania, limitów czasu oczekiwania na zajętość (ang. busy timeouts) czy przerywania instrukcji mogą odbiegać od natywnych kompilacji w języku C.
Stworzyliśmy gosuda.org/sqlite3, aby aplikacje napisane w Go nie musiały wybierać pomiędzy CGO_ENABLED=0 a prawdziwym silnikiem C SQLite. Poprzez skompilowanie oficjalnego kodu źródłowego C SQLite 3.54.1 do dedykowanego serwera pamięci współdzielonej (hqq-sqlite-server) i komunikację za pośrednictwem gosuda.org/hqq, Twój plik binarny Go kompiluje się z flagą CGO_ENABLED=0, jednocześnie wykonując zmodyfikowany, zoptymalizowany pod kątem -O3 kod C SQLite poza granicami procesu.
Wykonanie bez Konfiguracji: Od go get do sql.Open
Zastąpienie github.com/mattn/go-sqlite3 w istniejącej bazie kodu Go wymaga zmiany wyłącznie ścieżki importu pustego pakietu. Sterownik rejestruje się jako "sqlite3" (oraz "hqq-sqlite" w celu jawnego wyboru) i akceptuje te same łańcuchy zapytań 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}
Osadzone Pliki Binarne Procesu Roboczego Poświadczone przez SLSA
Naturalnym pytaniem podczas słuchania o tym, że biblioteka używa procesu roboczego C, jest: „Czy muszę instalować lub dystrybuować osobny plik binarny C wraz z moim programem w Go?”
Nie. gosuda.org/sqlite3 osadza wstępnie skompilowane pliki binarne hqq-sqlite-server dla wszystkich sześciu głównych platform docelowych bezpośrednio wewnątrz plików Go oznaczonych etykietami platform (internal/embedded/embed_<goos>_<goarch>.go). Ponieważ ograniczenia kompilacji w Go kompilują wyłącznie plik pasujący do docelowych zmiennych GOOS i GOARCH, kompilacja skrośna za pomocą polecenia GOOS=linux GOARCH=arm64 go build automatycznie osadza jedynie plik binarny serwera dla architektury linux/arm64 o rozmiarze około 1,4 MiB.
| Docelowy System Operacyjny | Docelowa Architektura (GOARCH) | Osadzony Plik Binarny | Łańcuch Narzędzi Kompilacji i Pochodzenie |
|---|---|---|---|
| Linux | amd64, arm64 | hqq-sqlite-server_linux_<arch> | GCC -O3, Pochodzenie SLSA Build L3 |
| macOS | arm64, amd64 | hqq-sqlite-server_darwin_<arch> | AppleClang -O3, Pochodzenie SLSA Build L3 |
| Windows | amd64 | hqq-sqlite-server_windows_amd64.exe | MSVC -O2, Pochodzenie SLSA Build L3 |
| Windows | arm64 | hqq-sqlite-server_windows_arm64.exe | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Gdy sql.Open ustanawia swoje pierwsze połączenie, plik autolaunch.go materializuje osadzony plik binarny w katalogu pamięci podręcznej użytkownika ($XDG_CACHE_HOME/hqq/bin/<sha256>/), weryfikuje jego kryptograficzny skrót SHA-256 względem przypisanej mapy ReleaseHashes w pliku hashes.go za pośrednictwem gosuda.org/hqq/secureexec, a następnie uruchamia proces roboczy z flagą --parent-stdin, dzięki czemu jego cykl życia jest ściśle powiązany z Twoim procesem Go.
Zgodność z DSN w Stylu Clean-Room dla mattn/go-sqlite3
Aby działać jako prawdziwy zamiennik typu drop-in, parser DSN (dsn.go) implementuje replikę typu clean-room parametrów połączenia i pragmatyk używanych w bazach kodu mattn/go-sqlite3:
| Parametr Zapytania DSN | Akceptowane Wartości | Efekt w SQLite |
|---|---|---|
_busy_timeout / _timeout | Liczba całkowita w milisekundach (np. 5000) | Wywołuje sqlite3_busy_timeout(db, ms) podczas otwierania sesji. |
_journal_mode / _journal | DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF | Wykonuje PRAGMA journal_mode = <mode>. |
_txlock | immediate, exclusive, deferred | Wybiera BEGIN IMMEDIATE, BEGIN EXCLUSIVE lub BEGIN DEFERRED dla db.BeginTx. |
_foreign_keys / _fk | on, true, 1, off, false, 0 | Wykonuje PRAGMA foreign_keys = ON/OFF. |
_synchronous / _sync | 0/OFF, 1/NORMAL, 2/FULL, 3/EXTRA | Wykonuje PRAGMA synchronous = <level>. |
_cache_size | Liczba całkowita stron lub -KiB (np. -20000) | Wykonuje PRAGMA cache_size = <val>. |
_loc | auto lub strefa czasowa IANA (np. UTC, Asia/Seoul) | Parsuje kolumny tekstowe DATETIME/TIMESTAMP do *time.Time w docelowej lokalizacji. |
mode | ro, rw, rwc, memory | Ustawia SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE lub SQLITE_OPEN_MEMORY. |
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuum | Wartości logiczne lub liczby całkowite trybu | Stosuje odpowiednią pragmatykę sesji (PRAGMA) podczas inicjalizacji połączenia. |
Przekształcanie Izolacji Procesów w Przewagę Wydajnościową
Na pierwszy rzut oka uruchomienie SQLite w osobnym procesie wydaje się wolniejsze niż wywoływanie sqlite3_step wewnątrz procesu za pomocą cgo. W praktyce dzieje się odwrotnie, gdy tylko zapytanie zwraca więcej niż jeden wiersz.
Ukryty Koszt Wywołań cgo Wiersz po Wierszu
W konwencjonalnym sterowniku SQLite opartym na cgo, pakiet database/sql steruje iteracją po jednym wierszu i jednej kolumnie naraz. Odczytanie 1 000 wierszy z 4 kolumnami wywołuje:
- 1 000 wywołań
cgodosqlite3_step - 4 000 wywołań
cgodosqlite3_column_typeorazsqlite3_column_int64/sqlite3_column_text - Tysiące pojedynczych przekroczeń granic między harmonogramem zadań (ang. scheduler) Go a stosem C
Potokowanie Żądań oraz Prefetching Wsadowy 64 KiB
gosuda.org/sqlite3 eliminuje przekraczanie granic dla każdego wiersza poprzez przeprojektowanie protokołu sieciowego w oparciu o dwa mechanizmy zawarte w plikach stmt.go, rows.go oraz 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
- Potokowanie Zapytań w Jednym Przelewie (
stmt.go): KiedyQueryContextlubStmt.QueryContextwykonuje instrukcję tylko do odczytu, funkcjas.query()przesyła asynchronicznie operacjereset,bind_values,stmt_info(przy pierwszym wykonaniu) orazfetch_rowsw ramach jednego przelewu pamięci współdzielonej. Wszystkie cztery operacje wykonują się bezpośrednio jedna po drugiej w procesie roboczym C i zwracają wynik w jednym cyklu żądanie-odpowiedź (ang. round trip). - Ciasne Pętle Wsadowe C oraz Asynchroniczny Prefetch (
rows.go): Wewnątrzfetch_rows(sqlite3_hooks.c) proces roboczy C wykonuje krokisqlite3_step(stmt)w natywnej pętli C, kodując maksymalnie liczbę wierszy określoną przezFetchSize(domyślnie 128) bezpośrednio w 64-kilobajtowym gnieździe odpowiedzi pamięci współdzielonej, aż do osiągnięcia limitu wsadu, zapełnienia gniezta lub wystąpienia stanuSQLITE_DONE. Jak tylkorows.ingest()zdekoduje wsad Batch 1, uruchamia onFetchRowsAsyncdla wsadu Batch 2 przed zwróceniem pierwszego wiersza wywołującemu. Podczas gdy Twój kod Go skanuje wsad Batch 1, proces roboczy C przetwarza już wsad Batch 2 na innym rdzeniu procesora.
Na procesorze Apple M4 (darwin/arm64), porównanie biblioteki gosuda.org/sqlite3 (CGO_ENABLED=0) z github.com/mattn/go-sqlite3 (CGO_ENABLED=1) na tej samej bazie danych SQLite pokazuje efekt stosowania wsadowania i potokowania:
| Obciążenie Robocze | gosuda.org/sqlite3 (CGO_ENABLED=0) | mattn/go-sqlite3 (CGO_ENABLED=1) | Względna Szybkość |
|---|---|---|---|
Wybór Punktowy (QueryRow po kluczu głównym) | 3.52 µs/op (9 B, 1 alokacja) | 3.56 µs/op (232 B, 11 alokacji) | 1,01x (parytet, 11x mniej alokacji) |
Skanowanie 100 Wierszy (rows.Next) | 20.41 µs/op (30,5 KB, 211 alokacji) | 36.34 µs/op (21,8 KB, 309 alokacji) | 1,78x szybciej |
Skanowanie 1 000 Wierszy (rows.Next) | 125.92 µs/op (296 KB, 2 030 alokacji) | 349.10 µs/op (216 KB, 3 009 alokacji) | 2,77x szybciej |
| Przygotowane Wstawianie (Autocommit) | 5.33 µs/op (0 B, 0 alokacji) | 5.88 µs/op (120 B, 5 alokacji) | 1,10x szybciej (0 alokacji) |
| Wsad Transakcyjny 100 Wierszy | 189.99 µs/op (16 B, 1 alokacja) | 191.50 µs/op (12,1 KB, 503 alokacje) | 1,01x (500x mniej alokacji) |
Anulowanie, Piaskownicowanie Ścieżek oraz Odzyskiwanie Puli Połączeń
Uruchamianie SQLite w wyizolowanym procesie wzmacnia również sposób, w jaki sterownik radzi sobie z limitami czasu (ang. timeouts), niezaufanym kodem SQL oraz granicami systemów plików:
- Deterministyczne Anulowanie Kontekstu (
conn.goorazerrors.go): Każde wywołanie sterownika uzbraja lekki strażnikcontext.AfterFunc(cn.guard). Jeślicontext.Contextwywołującego zostanie anulowany lub jego termin ważności wygaśnie podczas wykonywania powolnego zapytania rekurencyjnego lub oczekiwania na blokadę na serwerze, strażnik oznaczy połączenie jako wadliwe, zakończy lokalne połączenie sesyjne i wyśle ramkęCancel()poza pasem (ang. out-of-band). Proces roboczy C wywołuje natychmiast funkcjęsqlite3_interrupt(db), odblokowującsqlite3_step, podczas gdy sterownik Go mapuje wynikowy statusSQLITE_INTERRUPTnacontext.Canceledlubcontext.DeadlineExceededoraz instruujedatabase/sql, aby zastąpić połączenie w puli. - Piaskownicowanie Katalogów (
sqlsrv_pathrules.c): Proces roboczy C wymusza reguły ścieżki--allow-dirprzy każdym wywołaniuopen_v2, rozwiązując dowiązania symboliczne (ang. symlinks) i odrzucając względną nawigację..poza dozwolony katalog. Funkcjesqlite3_load_extensionorazSQLITE_LIMIT_ATTACHEDsą domyślnie wyłączone, aby wstrzyknięcie kodu SQL (ang. SQL injection) nie mogło uciec z katalogu bazy danych ani załadować dowolnych obiektów współdzielonych.
Struktura Repozytorium
Repozytorium jest ustrukturyzowane w taki sposób, że pakiet najwyższego poziomu dostarcza sterownik database/sql typu drop-in, podczas gdy katalogi launcher/ i sqlite3/ pozostają dostępne dla zaawansowanych wdrożeń niestandardowych serwerów:
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)
Wprowadzenie
Zainstaluj moduł za pomocą polecenia go get i otwórz swoją bazę danych za pomocą sql.Open("sqlite3", dsn):
1go get gosuda.org/sqlite3@v0.3.3540100
- Repozytorium GitHub: https://github.com/gosuda/hqq-sqlite3
- Dokumentacja DSN i Pragm: docs/DSN.md
- Architektura i Protokół Sieciowy: docs/ARCHITECTURE.md