SQLite na RAM: serialize e deserialize em Go
O SQLite pode rodar inteiramente na memória, sem nunca tocar o disco. E tem um truque pouco conhecido: dá para tirar um snapshot dos bytes do próprio banco e recarregá-lo depois, sem INSERT, sem reconstruir nada. As funções são sqlite3_serialize e sqlite3_deserialize.
Vou usar o driver modernc.org/sqlite, que é Go puro, sem CGo.
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
fatal(err)
}
db.SetMaxOpenConns(1)
A pegadinha está no SetMaxOpenConns(1). Um banco :memory: pertence à conexão que o abriu. Sem limitar a uma conexão, o pool do database/sql poderia abrir outra e essa veria um banco vazio e diferente.
Essas funções não estão na interface do database/sql. Chegamos nelas pelo escape hatch (*sql.Conn).Raw, usando uma interface que o driver implementa:
type serializer interface {
Serialize() ([]byte, error)
Deserialize([]byte) error
}
O snapshot devolve, byte a byte, o que seria o arquivo .sqlite em disco. Os primeiros 16 bytes são literalmente SQLite format 3\0:
func snapshot(db *sql.DB) ([]byte, error) {
conn, err := db.Conn(context.Background())
if err != nil {
return nil, err
}
defer func() { _ = conn.Close() }()
var buf []byte
err = conn.Raw(func(dc any) error {
s, ok := dc.(serializer)
if !ok {
return errors.New("the driver does not expose Serialize")
}
var serr error
buf, serr = s.Serialize()
return serr
})
return buf, err
}
O restore faz o caminho inverso. Depois dele, todas as tabelas e linhas estão de volta na memória.
func restore(db *sql.DB, buf []byte) error {
conn, err := db.Conn(context.Background())
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
return conn.Raw(func(dc any) error {
s, ok := dc.(serializer)
if !ok {
return errors.New("the driver does not expose Deserialize")
}
return s.Deserialize(buf)
})
}
Você processa tudo em memória, rápido e sem deixar nada em texto puro no disco, e quando quiser persistir salva um único blob. Esse blob pode ir para o disco cifrado e aí ninguém lê o banco sem a chave.