Help us shape the future of Go! Take the 2026 Go Developer Survey

wasipg

package module
v0.0.0-...-fa290be Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

wasipg

Embedded PostgreSQL for Go — a WASI build of PostgreSQL running in-process under wazero, with working error recovery. Pure Go: no cgo, no Docker, no native binaries.

Status: pre-alpha. The engine works end-to-end (PostgreSQL 17.5, real pgx clients, erroring queries recovered in-instance — see the conformance suite), but no bundle release is published yet and APIs may change without notice.

Usage

eng, err := wasipg.Start(ctx, wasipg.Options{
	// Until the first release is published, point at artifacts from
	// ./build/build.sh; a released wasipg.DefaultBundle (pinned
	// version + sha256, auto-fetched and cached) replaces this.
	Bundle: wasipg.LocalBundle("build/out"),
})
if err != nil { /* ... */ }
defer eng.Close()

conn, err := pgx.ConnectConfig(ctx, eng.PGXConfig())

SELECTs, DDL, transactions, and — the whole point — erroring queries behave like a real PostgreSQL: a syntax error or constraint violation comes back as a normal wire-level ERROR and the same instance keeps serving. Engines are single-connection and disposable (wasipg.NewPool reboots fresh ones from the pristine bundle); data is ephemeral unless you pass Options.Dir.

Note that the engine is not a security boundary: it runs in your process and the wire-level password handshake is not actually verified by the upstream WASI backend. Treat it like SQLite, not like a network-exposed server.

Why

PGlite proved PostgreSQL runs well as WASM, but its WASI builds compile with setjmp/longjmp disabled: every PostgreSQL ERROR escalates to abort() and kills the whole instance. That makes them unusable for any workload where erroring queries are normal input (tests, dev tools, anything driven by user SQL). The JS/Emscripten builds recover; the WASI builds do not — and there is no maintained, error-recovering embedded PostgreSQL for Go at all.

wasipg's bet: PostgreSQL's own error recovery (sigsetjmp-based, and fully functional in single-user mode on native builds) works in WASI too, once the build enables wasi-sdk's setjmp/longjmp support — which lowers onto the standardized wasm exception-handling proposal — and the runtime implements that proposal, which wazero does as of v1.12.

The stage-0 gate (passed, 2026-08)

gate/ pins the toolchain/runtime contract with a minimal reproduction of PostgreSQL's error-recovery shape: a long-lived jmp_buf, errors raised deep in the call stack via longjmp, control returning to a top-level loop that keeps serving.

Piece Requirement
Compiler wasi-sdk ≥ 33 (LLVM 22): -mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh=false + -lsetjmp
EH flavor standardized exnref (try_table/throw_ref) — LLVM's default legacy opcodes are rejected by wazero
Runtime wazero ≥ 1.12: WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling)
$ go test ./gate/
ok  	github.com/moznion/wasipg/gate

Roadmap

The full implementation plan — work items, exit criteria, risks, and fallbacks per stage — lives in docs/PLAN.md. Summary:

  1. Bundle build (done) — a self-contained WASI PostgreSQL 17.5 bundle (pglite.wasi + runtime FS + pristine initdb'd data dir) built reproducibly from electric-sql/postgres-pglite, Electric's maintained PostgreSQL fork: ./build/build.sh, then go run ./cmd/smoke boots it under wazero and drives it with pgx (boot from pristine data: ~50 ms).
  2. sjlj build (done) — the same bundle built with setjmp/longjmp lowered onto standardized exnref exception handling (wasi-sdk 33). A real pgx client gets wire-level ERRORs (correct SQLSTATEs) and the same instance keeps serving — 120 mixed error/success iterations without a trap, aborted-transaction semantics intact (go test ./conformance/). Cost vs the non-sjlj build: +0.2% module size, no measurable boot/latency change.
  3. Go library (mostly done) — wasipg.Start / Engine / PGXConfig / Pool with sha256-verified bundle fetch and caching; conformance suite in CI (Linux/macOS). Remaining: first tagged release publishing the bundle (which fills in wasipg.DefaultBundle) and wiring into a real consumer.

Design intent: the bundle-loading seam stays abstract so artifacts can later be swapped for official libpglite releases when those ship.

Non-goals (initial)

  • Multiple concurrent connections (the engine is single-connection; pool instances instead).
  • Durability guarantees for long-lived data. The first target workloads are ephemeral: tests, dev databases, static analysis oracles.
  • Extensions beyond what the bundle ships (initially plpgsql only).

Acknowledgements

This project stands on PGlite by Electric — specifically the postgres-pglite patch set that makes single-process PostgreSQL speak the wire protocol — and on pmp-p's pioneering WASI build work. PostgreSQL itself is © the PostgreSQL Global Development Group, under the PostgreSQL License.

License

Go code and build recipes: Apache-2.0 (see LICENSE).

PostgreSQL and postgres-pglite-derived material remains under its upstream licenses and is not covered by that grant: the released bundle artifacts are PostgreSQL 17.5 builds under the PostgreSQL License (shipped as COPYRIGHT inside pglite-wasi.tar.xz and attached to each release), and the diffs under build/patches/ are modifications of that source (see build/patches/README.md). NOTICE carries the full attribution, including the statically linked zlib and wasi-libc.

Documentation

Overview

Package wasipg is an embedded PostgreSQL for Go: a WASI build of PostgreSQL (Electric's postgres-pglite fork, compiled with working setjmp/longjmp error recovery) executed in-process by wazero and exposed through the standard wire protocol, so any pgx/lib/pq client works. No cgo, no Docker, no native binaries.

eng, err := wasipg.Start(ctx, wasipg.Options{})
if err != nil { ... }
defer eng.Close()
conn, err := pgx.ConnectConfig(ctx, eng.PGXConfig())

An Engine is single-connection and disposable: erroring queries are recovered by PostgreSQL itself, but a wasm-level trap marks the engine dead and it must be replaced (see Pool). Data is ephemeral by default; pass Options.Dir to keep state across runs.

Index

Constants

This section is empty.

Variables

View Source
var DefaultBundle = Bundle{}

DefaultBundle is the pinned released bundle. Until the first release is published it is empty; Start returns a descriptive error telling the caller to either upgrade or point Options.Bundle at local artifacts.

Functions

This section is empty.

Types

type Bundle

type Bundle struct {
	// BundlePath / PGDataPath point at local tarballs. When set they
	// take precedence over the URLs and no network access happens.
	BundlePath string
	PGDataPath string

	// BundleURL / PGDataURL are fetched into CacheDir (or the user
	// cache dir) on first use.
	BundleURL    string
	BundleSHA256 string
	PGDataURL    string
	PGDataSHA256 string
}

Bundle identifies the two artifacts an engine boots from: the module bundle (pglite.wasi + its runtime filesystem) and the pristine initdb'd data directory. Each comes either from a local path (air-gapped / vendored use) or from a URL fetched once into the user cache directory and verified against a pinned sha256. The library refuses hash-mismatched bundles.

func LocalBundle

func LocalBundle(outDir string) Bundle

LocalBundle points at artifacts produced by ./build/build.sh in a checkout of this repository.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is one booted, single-connection PostgreSQL instance.

func Start

func Start(ctx context.Context, opts Options) (*Engine, error)

Start boots a PostgreSQL engine: resolves (and if necessary fetches and verifies) the bundle, extracts it into the working directory, seeds the pristine data directory, and runs the module under wazero until it is ready to accept a wire connection.

func (*Engine) Close

func (e *Engine) Close() error

Close shuts the engine down and releases the runtime. Ephemeral working directories are removed.

func (*Engine) ConnString

func (e *Engine) ConnString() string

ConnString returns a connection string for the engine. The host is symbolic — the dial is intercepted in-process.

func (*Engine) Dead

func (e *Engine) Dead() bool

Dead reports whether the instance trapped and must be disposed.

func (*Engine) Dial

func (e *Engine) Dial(ctx context.Context, network, addr string) (net.Conn, error)

Dial returns the engine's in-process wire connection. The engine is strictly single-connection: a second dial fails until the engine is replaced. Signature matches pgconn's DialFunc.

func (*Engine) Dir

func (e *Engine) Dir() string

Dir returns the engine's working directory (containing the data dir and pg.log). For ephemeral engines it disappears on Close.

func (*Engine) PGXConfig

func (e *Engine) PGXConfig() *pgx.ConnConfig

PGXConfig returns a pgx connection config wired to the in-process engine.

type Options

type Options struct {
	// Dir is the working directory holding the extracted bundle and
	// the data directory. "" means an ephemeral temp dir removed on
	// Close. A Dir whose data directory already exists is resumed
	// (no initdb, no pgdata extraction). A Dir belongs to at most one
	// live Engine: booting two engines on the same Dir is undefined
	// behavior (they would share PGDATA and the wire-pump files).
	Dir string

	// Bundle selects the artifacts to boot. Zero value means
	// DefaultBundle.
	Bundle Bundle

	// CacheDir overrides the cache location for fetched bundles and
	// wazero's compilation cache. "" means os.UserCacheDir()/wasipg.
	CacheDir string
}

Options configures Start.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool hands out single-connection engines and replaces used or dead ones by booting fresh instances from the pristine bundle. Engines that lived through a trap are never reused — dispose-and-reboot is the recovery contract for crash-class faults.

func NewPool

func NewPool(opts Options) *Pool

NewPool returns a pool that boots engines with opts. Options.Dir must be empty: pooled engines are always ephemeral.

func (*Pool) Close

func (p *Pool) Close()

Close disposes all idle engines. Engines currently handed out are the caller's to close.

func (*Pool) Get

func (p *Pool) Get(ctx context.Context) (*Engine, error)

Get returns a fresh engine, either prewarmed or booted on demand. The caller owns it: hand it back with Put (or Close it directly).

func (*Pool) Prewarm

func (p *Pool) Prewarm(ctx context.Context, n int) error

Prewarm boots n engines in the background of the caller so later Gets are instant. Errors abort prewarming and are returned.

func (*Pool) Put

func (p *Pool) Put(e *Engine)

Put returns an engine to the pool. Engines that were dialed, died, or were closed are disposed of — a pooled engine is always pristine.

Directories

Path Synopsis
cmd
mkdata command
Command mkdata produces the pristine initdb'd PGDATA tarball that ships alongside the bundle: it boots the bundle once in a fresh workdir (which runs in-guest initdb), shuts the engine down cleanly, and packs the resulting data directory.
Command mkdata produces the pristine initdb'd PGDATA tarball that ships alongside the bundle: it boots the bundle once in a fresh workdir (which runs in-guest initdb), shuts the engine down cleanly, and packs the resulting data directory.
smoke command
Command smoke is the stage-1 exit-criteria probe: boot the from-source bundle (build/out/) under wazero, connect with pgx over the socket-file wire pump, and run version/DDL/DML/catalog queries on a clean data dir.
Command smoke is the stage-1 exit-criteria probe: boot the from-source bundle (build/out/) under wazero, connect with pgx over the socket-file wire pump, and run version/DDL/DML/catalog queries on a clean data dir.
internal
engine
Package engine boots the wasipg WASI PostgreSQL bundle under wazero and exposes its socket-file wire pump.
Package engine boots the wasipg WASI PostgreSQL bundle under wazero and exposes its socket-file wire pump.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL