gosuda.org/sqlite3: Driver database/sql com CGO_ENABLED=0 sobre IPC de Memória Compartilhada

gosuda.org/sqlite3 é um driver database/sql para Go com CGO_ENABLED=0 que executa o amalgamation C oficial do SQLite 3.54.1 upstream dentro de um processo worker isolado, comunicando-se por meio de anéis de memória compartilhada HQQ. Ele se registra com o nome de driver padrão "sqlite3", oferece suporte aos parâmetros de consulta do DSN de github.com/mattn/go-sqlite3, incorpora binários de servidor pré-compilados com atestação SLSA para seis alvos de sistema operacional e arquitetura, e utiliza pipelining de requisições com pré-busca de lotes de 64 KiB para superar o cgo em processo em consultas de múltiplas linhas.
1go get gosuda.org/sqlite3@v0.3.3540100
Por que Mais um Driver SQLite para Go?
O SQLite é um dos softwares mais implantados do mundo, porém, usá-lo a partir de Go historicamente obrigou os engenheiros a uma escolha incômoda entre simplicidade de build e fidelidade do motor.
De um lado está o github.com/mattn/go-sqlite3, o padrão consolidado da comunidade. Ele envolve diretamente o amalgamation C oficial do SQLite por meio de cgo. Obtém-se o planejador de consultas C real, a semântica completa de concorrência do WAL e o gerenciamento de memória C previsível. O custo é CGO_ENABLED=1: builds locais e pipelines de CI tornam-se mais lentos, imagens de contêiner mínimas (scratch ou distroless) exigem bibliotecas de runtime C correspondentes, a compilação cruzada para outro sistema operacional ou arquitetura de CPU requer um cross-toolchain C completo, e qualquer corrupção de memória no código C compartilha o espaço de endereçamento do processo Go.
Do outro lado estão os ports puros em Go do SQLite, gerados pela tradução mecânica do amalgamation C para código-fonte Go sobre um runtime C emulado (libc). Eles restauram CGO_ENABLED=0, o que torna os builds triviais. Sob cargas de trabalho sustentadas, entretanto, o heap C traduzido e as alocações por linha em Go aumentam a pressão sobre o coletor de lixo, e casos sutis de bloqueio, timeouts de ocupação ou interrupção de statements podem divergir das builds C nativas.
Construímos o gosuda.org/sqlite3 para que aplicações Go não precisem escolher entre CGO_ENABLED=0 e o motor C real do SQLite. Ao compilar o amalgamation C oficial do SQLite 3.54.1 em um servidor dedicado de memória compartilhada (hqq-sqlite-server) e comunicar-se por meio de gosuda.org/hqq, seu binário Go compila com CGO_ENABLED=0 enquanto executa o SQLite em C não modificado e otimizado com -O3 através de uma fronteira de processo.
Execução Sem Configuração: Do go get ao sql.Open
Substituir o github.com/mattn/go-sqlite3 em uma base de código Go existente exige alterar apenas o caminho do import anônimo. O driver se registra como "sqlite3" (e como "hqq-sqlite" para seleção explícita) e aceita as mesmas strings de consulta do 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}
Binários Worker Incorporados e Atestados por SLSA
Uma pergunta natural ao saber que uma biblioteca utiliza um processo worker em C é: "Preciso instalar ou distribuir um binário C separado junto ao meu programa Go?"
Não. O gosuda.org/sqlite3 incorpora binários hqq-sqlite-server pré-compilados para todos os seis alvos principais, diretamente em arquivos Go com tags de plataforma (internal/embedded/embed_<goos>_<goarch>.go). Como as restrições de build do Go compilam apenas o arquivo correspondente ao seu GOOS e GOARCH alvo, a compilação cruzada com GOOS=linux GOARCH=arm64 go build incorpora automaticamente apenas o binário de servidor linux/arm64, de aproximadamente 1,4 MiB.
| Sistema Operacional Alvo | Arquitetura Alvo (GOARCH) | Binário Incorporado | Toolchain de Build e Proveniência |
|---|---|---|---|
| Linux | amd64, arm64 | hqq-sqlite-server_linux_<arch> | GCC -O3, Proveniência SLSA Build L3 |
| macOS | arm64, amd64 | hqq-sqlite-server_darwin_<arch> | AppleClang -O3, Proveniência SLSA Build L3 |
| Windows | amd64 | hqq-sqlite-server_windows_amd64.exe | MSVC -O2, Proveniência SLSA Build L3 |
| Windows | arm64 | hqq-sqlite-server_windows_arm64.exe | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Quando o sql.Open estabelece sua primeira conexão, o autolaunch.go materializa o binário incorporado no diretório de cache do usuário ($XDG_CACHE_HOME/hqq/bin/<sha256>/), verifica seu digest SHA-256 criptográfico contra o mapa fixado ReleaseHashes em hashes.go por meio de gosuda.org/hqq/secureexec, e inicia o worker com --parent-stdin, de modo que seu tempo de vida fique estritamente vinculado ao seu processo Go.
Compatibilidade de DSN com mattn/go-sqlite3 por Implementação Independente (Clean-Room)
Para funcionar como um substituto direto genuíno, o analisador de DSN (dsn.go) implementa uma réplica clean-room dos parâmetros de conexão e de pragmas utilizados em bases de código mattn/go-sqlite3:
| Parâmetro de Consulta do DSN | Valores Aceitos | Efeito no SQLite |
|---|---|---|
_busy_timeout / _timeout | Inteiro em milissegundos (ex.: 5000) | Chama sqlite3_busy_timeout(db, ms) na abertura da sessão. |
_journal_mode / _journal | DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF | Executa PRAGMA journal_mode = <mode>. |
_txlock | immediate, exclusive, deferred | Seleciona BEGIN IMMEDIATE, BEGIN EXCLUSIVE ou BEGIN DEFERRED para db.BeginTx. |
_foreign_keys / _fk | on, true, 1, off, false, 0 | Executa PRAGMA foreign_keys = ON/OFF. |
_synchronous / _sync | 0/OFF, 1/NORMAL, 2/FULL, 3/EXTRA | Executa PRAGMA synchronous = <level>. |
_cache_size | Inteiro em páginas ou -KiB (ex.: -20000) | Executa PRAGMA cache_size = <val>. |
_loc | auto ou fuso horário IANA (ex.: UTC, Asia/Seoul) | Converte colunas de texto DATETIME/TIMESTAMP em *time.Time no local de destino. |
mode | ro, rw, rwc, memory | Define SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE ou SQLITE_OPEN_MEMORY. |
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuum | Booleano ou inteiros de modo | Aplica o PRAGMA de sessão correspondente na inicialização da conexão. |
Transformando o Isolamento de Processo em Vantagem de Vazão
À primeira vista, executar o SQLite em um processo separado parece necessariamente mais lento do que chamar sqlite3_step em processo via cgo. Na prática, ocorre o oposto assim que uma consulta retorna mais de uma única linha.
O Custo Oculto das Chamadas cgo Linha a Linha
Em um driver SQLite convencional baseado em cgo, o database/sql conduz a iteração uma linha e uma coluna por vez. Ler 1.000 linhas com 4 colunas dispara:
- 1.000 chamadas
cgoasqlite3_step - 4.000 chamadas
cgoasqlite3_column_typeesqlite3_column_int64/sqlite3_column_text - Milhares de travessias individuais de fronteira entre o escalonador do Go e a pilha C
Pipelining de Requisições e Pré-busca de Lotes de 64 KiB
O gosuda.org/sqlite3 elimina as travessias de fronteira por linha ao redesenhar o protocolo de comunicação em torno de dois mecanismos nos arquivos stmt.go, rows.go e sqlite3/server/sqlite3_hooks.c:
1Cliente Go (stmt.go / rows.go) Worker C11 (sqlite3_hooks.c)
2------------------------------ ----------------------------
31. Pipeline em 1 voo:
4 ResetAsync + BindValuesAsync
5 + StmtInfoAsync + FetchRowsAsync(128) =======> reset -> bind -> stmt_info ->
6 loop sqlite3_step() até 128 linhas
7 <======= Lote 1 (linhas 1..128 empacotadas em slot de 64 KiB)
82. Disparar imediatamente a pré-busca:
9 FetchRowsAsync(128) (Lote 2) =======> loop sqlite3_step() para as linhas 129..256
10 Enquanto o C avança o Lote 2, o chamador
11 Go lê as linhas 1..128 do Lote 1! <======= Lote 2 pronto na memória compartilhada
- Pipelining de Consulta em Voo Único (
stmt.go): QuandoQueryContextouStmt.QueryContextexecuta um statement somente leitura,s.query()submetereset,bind_values,stmt_info(na primeira execução) efetch_rowsde forma assíncrona em um único voo de memória compartilhada. Todas as quatro operações são executadas em sequência no worker C e retornam em uma única ida e volta. - Laços Compactos de Lote em C e Pré-busca Assíncrona (
rows.go): Dentro defetch_rows(sqlite3_hooks.c), o worker C executasqlite3_step(stmt)em um laço nativo em C, codificando atéFetchSizelinhas (padrão 128) diretamente no slot de resposta de 64 KiB da memória compartilhada, até que o limite do lote seja atingido, o slot esteja cheio ou ocorraSQLITE_DONE. Assim que orows.ingest()decodifica o Lote 1, ele disparaFetchRowsAsyncpara o Lote 2 antes de devolver a primeira linha ao chamador. Enquanto seu código Go lê o Lote 1, o worker C já está avançando o Lote 2 em outro núcleo de CPU.
No Apple M4 (darwin/arm64), a comparação entre gosuda.org/sqlite3 (CGO_ENABLED=0) e github.com/mattn/go-sqlite3 (CGO_ENABLED=1) sobre o mesmo banco de dados SQLite mostra o efeito do agrupamento em lotes e do pipelining:
| Carga de Trabalho | gosuda.org/sqlite3 (CGO_ENABLED=0) | mattn/go-sqlite3 (CGO_ENABLED=1) | Velocidade Relativa |
|---|---|---|---|
Seleção Pontual (QueryRow por PK) | 3,52 µs/op (9 B, 1 alocação) | 3,56 µs/op (232 B, 11 alocações) | 1,01x (paridade, 11x menos alocações) |
Leitura de 100 Linhas (rows.Next) | 20,41 µs/op (30,5 KB, 211 alocações) | 36,34 µs/op (21,8 KB, 309 alocações) | 1,78x mais rápido |
Leitura de 1.000 Linhas (rows.Next) | 125,92 µs/op (296 KB, 2.030 alocações) | 349,10 µs/op (216 KB, 3.009 alocações) | 2,77x mais rápido |
| Inserção Preparada (Autocommit) | 5,33 µs/op (0 B, 0 alocações) | 5,88 µs/op (120 B, 5 alocações) | 1,10x mais rápido (0 alocações) |
| Lote de Transação de 100 Linhas | 189,99 µs/op (16 B, 1 alocação) | 191,50 µs/op (12,1 KB, 503 alocações) | 1,01x (500x menos alocações) |
Cancelamento, Isolamento de Caminhos e Recuperação do Pool de Conexões
Executar o SQLite em um worker isolado também fortalece a forma como o driver lida com timeouts, SQL não confiável e fronteiras do sistema de arquivos:
- Cancelamento Determinístico de Contexto (
conn.goeerrors.go): Cada chamada do driver arma uma guarda levecontext.AfterFunc(cn.guard). Se ocontext.Contextdo chamador for cancelado ou seu prazo expirar enquanto uma consulta recursiva lenta ou uma espera por bloqueio estiver em execução no servidor, a guarda marca a conexão como inválida, encerra o vínculo local de sessão e envia um quadroCancel()fora de banda. O worker C invocasqlite3_interrupt(db)imediatamente, desbloqueandosqlite3_step, enquanto o driver Go mapeia o statusSQLITE_INTERRUPTresultante paracontext.Canceledoucontext.DeadlineExceedede instrui odatabase/sqla substituir a conexão no pool. - Sandbox de Diretórios (
sqlsrv_pathrules.c): O worker C aplica as regras de caminho--allow-direm toda chamada aopen_v2, resolvendo links simbólicos e rejeitando travessias relativas..para fora do diretório permitido.sqlite3_load_extensioneSQLITE_LIMIT_ATTACHEDsão desabilitados por padrão, de modo que injeção de SQL não possa escapar do diretório do banco de dados nem carregar objetos compartilhados arbitrários.
Layout do Repositório
O repositório é estruturado de modo que o pacote de nível superior forneça o driver database/sql de substituição direta, enquanto launcher/ e sqlite3/ permanecem acessíveis para implantações avançadas com servidor personalizado:
1gosuda.org/sqlite3
2├── driver.go / api.go # Registro do driver database/sql ("sqlite3") e API Connector
3├── dsn.go # Analisador de DSN clean-room compatível com mattn/go-sqlite3 e configuração de pragmas
4├── conn.go / tx.go # Ciclo de vida da sessão, guarda de contexto e máquina de estados de transação
5├── stmt.go / rows.go # Pipelining de requisições em voo único e pré-busca de lotes de 64 KiB
6├── errors.go # Códigos de erro primários/estendidos tipados do SQLite e correspondência de sentinelas
7├── autolaunch.go / hashes.go # Materialização zero-config do servidor incorporado e fixação SHA-256
8├── internal/embedded/ # Binários //go:embed com tags de plataforma (linux, darwin, windows)
9├── launcher/ # Supervisor de processo configurável para implantações de servidor personalizado
10└── sqlite3/ # Cliente Go hqq-bindgen gerado e código-fonte do servidor SQLite C11
11 └── server/ # Implementação do servidor C11 (sqlite3_hooks.c, sqlsrv_policy.c)
Primeiros Passos
Instale o módulo com go get e abra seu banco de dados com sql.Open("sqlite3", dsn):
1go get gosuda.org/sqlite3@v0.3.3540100
- Repositório no GitHub: https://github.com/gosuda/hqq-sqlite3
- Referência de DSN e Pragmas: docs/DSN.md
- Arquitetura e Protocolo de Comunicação: docs/ARCHITECTURE.md