navidrome/db/pglite/setup.go
Deluan 98555ca499 feat(db): spike an embedded PostgreSQL via PGlite under wazero
Adds db/pglite, a bridge that runs the PGlite WASI build of PostgreSQL 17.5
inside the Navidrome process with wazero and exposes it to database/sql
through pgx over the real wire protocol. Selected with DbPath = "pglite://<dir>".

The bridge handles what the WASI build cannot do on its own: it emulates dup2
onto stdin (wazero refuses fd_renumber on preopens), replays ParameterStatus
for connections after the first, synthesizes a missing ReadyForQuery after an
error, recovers from the trap every PG ERROR raises, and passes wire bytes
through shared wasm memory instead of files (round-trip floor 377 µs to 39 µs).
Several connections are accepted and serialized onto the single backend,
holding the session across a whole transaction and a whole handshake; a client
that disconnects mid-transaction gets a ROLLBACK.

db.go opens the pglite:// scheme, skips the SQLite-only migrations, and can
apply a translated schema from ND_PGLITE_SCHEMA. The shared count() helper now
adds its ORDER BY only for SQLite, since Postgres rejects it next to
count(distinct ...).

The wasm archive is not committed. The README explains how to build it and
lists the limits found: one session, one process (DevExternalScanner must be
off), shared session state, simple protocol only.
2026-09-04 18:29:58 -04:00

118 lines
3 KiB
Go

package pglite
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/rand"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// WASIBinary holds the pglite-wasi tar.gz contents, read from Config.Tarball on first use.
var WASIBinary []byte
// setupEnvironment extracts the archive on first run and returns the pglite.wasi bytes.
func setupEnvironment(dataDir string) (wasmBinary []byte, err error) {
pgBaseDir := filepath.Join(dataDir, "pglite")
versionFile := filepath.Join(pgBaseDir, "base", "PG_VERSION")
wasmFile := filepath.Join(pgBaseDir, "bin", "pglite.wasi")
if _, err := os.Stat(versionFile); err != nil && !fileExists(wasmFile) {
if WASIBinary == nil {
return nil, fmt.Errorf("pglite WASI binary not available (WASIBinary is nil)")
}
if err := extractTarGz(dataDir, WASIBinary); err != nil {
return nil, fmt.Errorf("extracting pglite-wasi.tar.gz: %w", err)
}
}
devDir := filepath.Join(dataDir, "dev")
if err := os.MkdirAll(devDir, 0o755); err != nil {
return nil, fmt.Errorf("creating dev dir: %w", err)
}
urandomPath := filepath.Join(devDir, "urandom")
if _, err := os.Stat(urandomPath); err != nil {
randomBytes := make([]byte, 256)
if _, err := rand.Read(randomBytes); err != nil {
return nil, fmt.Errorf("generating random bytes: %w", err)
}
if err := os.WriteFile(urandomPath, randomBytes, 0o644); err != nil {
return nil, fmt.Errorf("writing urandom: %w", err)
}
}
wasmPath := filepath.Join(pgBaseDir, "bin", "pglite.wasi")
wasmBinary, err = os.ReadFile(wasmPath)
if err != nil {
return nil, fmt.Errorf("reading pglite.wasi: %w", err)
}
return wasmBinary, nil
}
func extractTarGz(destDir string, data []byte) error {
gzReader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("opening gzip: %w", err)
}
defer gzReader.Close()
tarReader := tar.NewReader(gzReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading tar entry: %w", err)
}
// destDir is mounted as /tmp in the guest, so drop the archive's leading "tmp/".
name := header.Name
name = strings.TrimPrefix(name, "tmp/")
if name == "" {
continue
}
target := filepath.Join(destDir, name)
if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)) {
return fmt.Errorf("tar entry %q escapes destination", header.Name)
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
return err
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
if _, err := io.Copy(f, tarReader); err != nil {
f.Close()
return err
}
f.Close()
case tar.TypeSymlink:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
os.Remove(target)
if err := os.Symlink(header.Linkname, target); err != nil {
return err
}
}
}
return nil
}