|
| 1 | +# pgpm → PGlite (proof of concept) |
| 2 | + |
| 3 | +Proof that the **unmodified** pgpm migrate engine can deploy real migrations into |
| 4 | +[**PGlite**](https://github.com/electric-sql/pglite) — ElectricSQL's WASM build of |
| 5 | +Postgres ("SQLite for Postgres"): embedded, in-process, **no Postgres server or |
| 6 | +service container required**. |
| 7 | + |
| 8 | +```bash |
| 9 | +cd poc/pglite |
| 10 | +npm ci |
| 11 | +npm start # boots PGlite in-process, runs pgpm deploy -> verify -> revert, asserts |
| 12 | +``` |
| 13 | + |
| 14 | +This runs in CI via `.github/workflows/pglite-poc.yaml` with no `services:` block — |
| 15 | +the whole point: PGlite needs no external database. |
| 16 | + |
| 17 | +## What it does |
| 18 | + |
| 19 | +`run.mjs` boots a PGlite instance, exposes it over a socket with the official |
| 20 | +`pg-gateway` shim (`@electric-sql/pglite-socket`), and points the published |
| 21 | +`@pgpmjs/core` `PgpmMigrate` engine at it as if it were ordinary Postgres: |
| 22 | + |
| 23 | +``` |
| 24 | +pgpm (@pgpmjs/core, unmodified) --node-pg / TCP--> PGLiteSocketServer --> PGlite (WASM) |
| 25 | +``` |
| 26 | + |
| 27 | +It then asserts a full lifecycle against `module/` (a 4-change pgpm plan that ends |
| 28 | +in a `pgvector` column): |
| 29 | + |
| 30 | +- `initialize` — bootstraps the `pgpm_migrate` schema (tables + PL/pgSQL procs) |
| 31 | +- `deploy` — `schema → table → index → embedding` |
| 32 | +- `verify` — all four verified |
| 33 | +- data round-trip — a pgvector nearest-neighbor query returns the seeded row |
| 34 | +- `revert` — everything reverted, registry emptied |
| 35 | + |
| 36 | +## Why the code isn't quite drop-in (the findings this PoC pins down) |
| 37 | + |
| 38 | +The engine works against PGlite **today** with two call-site accommodations; a real |
| 39 | +`pglite` target would fold these into the engine/driver layer: |
| 40 | + |
| 41 | +1. **Single-connection / transaction affinity.** PGlite serializes everything and, |
| 42 | + while a transaction is open, pins the engine to that one connection. The engine's |
| 43 | + `deploy({useTransaction:true})` opens `BEGIN` on one pooled connection and calls |
| 44 | + `isDeployed()` on a *second* — which can never run on PGlite → deadlock. The PoC |
| 45 | + uses `useTransaction:false`. Proper fix: run all transaction-scoped work on the |
| 46 | + same client. (Also: the socket shim defaults to `maxConnections:1`; we raise it — |
| 47 | + PGlite still serializes, so multiplexing is safe.) |
| 48 | + |
| 49 | +2. **Extensions are provisioned out-of-band.** pgpm's `cleanSql` deliberately strips |
| 50 | + `CREATE EXTENSION` (and `BEGIN/COMMIT`) from migrations — extensions come from the |
| 51 | + environment (the `postgres-plus` image / `pgsql-test`'s `installExtensions()`), |
| 52 | + not migrations. So the PoC creates the extension at PGlite bootstrap, and PGlite |
| 53 | + additionally requires the extension registered in JS at construction |
| 54 | + (`PGlite.create({ extensions: { vector } })`). |
| 55 | + |
| 56 | +Extension availability — not pgpm — is the functional boundary. Bundled/available: |
| 57 | +`pg_trgm`, `citext`, `ltree`, `uuid_ossp`, `pgvector`, … Not available (no WASM / |
| 58 | +no background workers): `pg_cron`, `pg_partman`, the BM25 search ext; PostGIS is |
| 59 | +experimental. A module needing those simply won't deploy to PGlite. |
| 60 | + |
| 61 | +## Notes |
| 62 | + |
| 63 | +Standalone on purpose: this directory is **not** part of the pnpm workspace (its own |
| 64 | +`package.json` + `package-lock.json`, run with `npm ci`) so the PoC stays independent |
| 65 | +of the monorepo build. It depends on the *published* `@pgpmjs/core`; it could be |
| 66 | +repointed at the workspace build to track unreleased engine changes. |
0 commit comments