navidrome/db/pglite
Deluan e2c8a1930a fix(pglite): do not cut slow readers, and bind playlist rules as text
The bridge gave every reply write a 5 second deadline. Scanner phase 3
streams all touched albums through a cursor and runs queries per row on
other pooled connections, which queue on the single session, so a large
library could not drain the reply in time and the bridge closed the pipe
mid-message ('unexpected EOF'). Replies now block as long as the client
needs, and client connections are closed on shutdown instead.

Playlist rules were bound as []byte, which pgx sends as bytea; the text
column then stored the hex form and the JSON parser failed on it.
2026-09-04 22:25:13 -04:00
..
build fix(pglite): stop leaking every wire message's memory in the wasm module 2026-09-04 21:38:21 -04:00
concurrency_test.go feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
embed.go feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
pglite.go fix(pglite): do not cut slow readers, and bind playlist rules as text 2026-09-04 22:25:13 -04:00
pglite.wasi.gz fix(pglite): stop leaking every wire message's memory in the wasm module 2026-09-04 21:38:21 -04:00
pglite_suite_test.go feat(db): spike an embedded PostgreSQL via PGlite under wazero 2026-09-04 18:29:58 -04:00
pglite_test.go feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
proto_test.go feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
README.md fix(pglite): give the initial migration a valid goose timestamp 2026-09-04 20:45:22 -04:00
setup.go feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
stdin.go feat(db): spike an embedded PostgreSQL via PGlite under wazero 2026-09-04 18:29:58 -04:00

db/pglite — spike: PostgreSQL embedded in the Navidrome binary

On this branch Navidrome's only database is the PGlite WASI build of PostgreSQL 17.5, run inside the process with wazero (pure Go, no CGO) and reached through database/sql and pgx over the real PostgreSQL wire protocol. SQLite support has been removed from db/ and the persistence layer.

This is exploratory. It is not a supported way to run Navidrome. It starts, runs a full scan, logs in, and browses artists and albums through both the native and Subsonic APIs. Everything else is untested, and several paths still carry SQLite-only SQL (see "Not ported").

How it fits together

Navidrome → database/sql → pgx → in-process pipe → bridge → wasm memory → PostgreSQL (wasm)

The bridge speaks no SQL. It copies wire-protocol bytes, and only inspects message framing, the handshake, and the ReadyForQuery status byte. A unix socket is also listened on for external tools; both paths perform the same.

Schema

The 131 SQLite migrations are gone. db/migrations/20260905004449_initial_schema.sql is one goose migration with the whole schema, a crude mechanical translation of the SQLite one (text/bigint/boolean/timestamp/jsonb, no FTS5, no triggers), plus the seed rows the old migrations used to insert: the default library and the default transcodings. DbPath defaults to pglite://<DataFolder>/pglite.

The wasm module

pglite.wasi.gz (4.9 MB) is embedded with go:embed, decompressed in memory and compiled by wazero at startup; nothing is unpacked to disk. The compiled code is cached under the cache folder (Config.CacheDir), so only the first start pays the ~1.7 s compile. The share files PostgreSQL needs are inside the module (wasi-vfs). The cluster itself is created by initdb on the first start (~1.5 s, in a throwaway wasm instance) in <DataFolder>/pglite/pgdata; a short-lived backend on template1 then runs CREATE DATABASE navidrome, and the real backend starts on it in a fresh instance. Later starts run PostgreSQL's real startup with WAL recovery, and every close checkpoints through pgl_shutdown. Config.WasmOverride points at a different module file for experiments.

It is our own build: the published WASI builds are all compiled -O0, and rebuilding with -O2 is worth 2 to 3x on every query. The recipe and patches, including one C fix of our own, are in build/. It is built without the wizer snapshot (PGLITE_WIZER=0): a snapshot starts faster but freezes the pristine cluster's transaction counter and WAL position, so rows committed by an earlier run become invisible after a restart.

Connecting with psql

PGPASSWORD=x PGSSLMODE=disable psql -h /absolute/path/to/DataFolder -U postgres navidrome

The exact command is logged at startup. A password is required even though PGlite skips authentication. Plain SQL works; \dt and \d do not, because the build's system views are missing.

Known limits

  • One session. PGlite is a single PostgreSQL backend. The bridge accepts several connections and serializes them, holding the session across a whole transaction and a whole handshake. Four connections perform exactly like one. Concurrent queries on one transaction are not possible either (pgx refuses them), so library.RefreshStats runs its queries sequentially.
  • One process. DevExternalScanner is forced off: a scanner subprocess would open a second PGlite on the same data directory and corrupt it.
  • Session state is shared. SET, temp tables and the like leak between clients.
  • Simple protocol only. The WASI build mishandles extended-protocol portals, so the DSN pins default_query_exec_mode=simple_protocol, and untyped literals sometimes need explicit casts.
  • No COPY FROM STDIN, no backup/restore (navidrome backup returns an error).

Not ported (still SQLite SQL)

Full-text search (persistence/sql_search_fts.go, FTS5), smart-playlist criteria (model/criteria, persistence/criteria_sql.go: json_tree), plugin cleanup (persistence/plugin_cleanup.go), GetRandom and optimizePagination (rowid), natural sorting (dropped; lower() replaces collate nocase). The Go test suites that create an in-memory SQLite database no longer run.

Environment switches

variable effect
ND_PGLITE_TRACE log wire traffic and per-tick timings
ND_PGLITE_NOCMA force the file transport instead of shared memory