|
1 | | -# pgpm → PGlite (proof of concept) |
| 1 | +# pgpm → PGlite |
2 | 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**. |
| 3 | +<p align="center" width="100%"> |
| 4 | + <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" /> |
| 5 | +</p> |
7 | 6 |
|
8 | | -```bash |
| 7 | +<p align="center" width="100%"> |
| 8 | + <a href="https://github.com/constructive-io/constructive/actions/workflows/pglite.yaml"> |
| 9 | + <img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/pglite.yaml/badge.svg" /> |
| 10 | + </a> |
| 11 | + <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"> |
| 12 | + <img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/> |
| 13 | + </a> |
| 14 | +</p> |
| 15 | + |
| 16 | +A standalone, end-to-end demonstration that the **unmodified, published** `@pgpmjs/core` migrate engine can deploy real migrations into [**PGlite**](https://github.com/electric-sql/pglite) — ElectricSQL's WASM build of Postgres ("SQLite for Postgres"): embedded, in-process, with **no Postgres server or service container**. |
| 17 | + |
| 18 | +It proves the wire-protocol path (pgpm ↔ PGlite over a socket). The **in-process driver** path — `@pgpmjs/pglite-adapter` + `pglite-test`, with no socket — is the productized version and is covered by the `unit-tests (pglite …)` job in [`run-tests.yaml`](../../.github/workflows/run-tests.yaml). |
| 19 | + |
| 20 | +## Run |
| 21 | + |
| 22 | +```sh |
9 | 23 | cd poc/pglite |
10 | | -npm ci |
11 | | -npm start # boots PGlite in-process, runs pgpm deploy -> verify -> revert, asserts |
| 24 | +pnpm install --ignore-workspace |
| 25 | +pnpm start # boots PGlite in-process, runs pgpm deploy → verify → revert, asserts |
12 | 26 | ``` |
13 | 27 |
|
14 | | -This runs in CI via `.github/workflows/pglite.yaml` with no `services:` block — |
15 | | -the whole point: PGlite needs no external database. |
| 28 | +Runs in CI via [`.github/workflows/pglite.yaml`](../../.github/workflows/pglite.yaml) (job `pglite-socket-e2e`) with **no `services:` block** — the whole point: PGlite needs no external database. |
16 | 29 |
|
17 | | -## What it does |
| 30 | +## How it works |
18 | 31 |
|
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: |
| 32 | +`run.js` boots a PGlite instance, exposes it over a socket with the official `pg-gateway` shim (`@electric-sql/pglite-socket`), and points the published `@pgpmjs/core` `PgpmMigrate` engine at it as if it were ordinary Postgres: |
22 | 33 |
|
23 | 34 | ``` |
24 | | -pgpm (@pgpmjs/core, unmodified) --node-pg / TCP--> PGLiteSocketServer --> PGlite (WASM) |
| 35 | +pgpm (@pgpmjs/core, unmodified) ──node-pg / TCP──▶ PGLiteSocketServer ──▶ PGlite (WASM) |
25 | 36 | ``` |
26 | 37 |
|
27 | | -It then asserts a full lifecycle against `module/` (a 4-change pgpm plan that ends |
28 | | -in a `pgvector` column): |
| 38 | +It then asserts a full lifecycle against `module/` (a 4-change pgpm plan ending in a `pgvector` column): |
| 39 | + |
| 40 | +* **initialize** — bootstraps the `pgpm_migrate` schema (tables + PL/pgSQL procs) |
| 41 | +* **deploy** — `schema → table → index → embedding` |
| 42 | +* **verify** — all four verified |
| 43 | +* **data round-trip** — a pgvector nearest-neighbor query returns the seeded row |
| 44 | +* **revert** — everything reverted, registry emptied |
29 | 45 |
|
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 |
| 46 | +Assertions exit non-zero on any mismatch, so CI fails loudly on regression. |
35 | 47 |
|
36 | | -## Why the code isn't quite drop-in (the findings this PoC pins down) |
| 48 | +## The two accommodations this pins down |
37 | 49 |
|
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: |
| 50 | +The engine runs against PGlite **today** with two call-site accommodations; the productized in-process driver folds these into the adapter layer: |
40 | 51 |
|
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.) |
| 52 | +1. **Single-connection / transaction affinity.** PGlite serializes everything and, while a transaction is open, pins the engine to that one connection. `deploy({ useTransaction: true })` opens `BEGIN` on one pooled connection and calls `isDeployed()` on a *second* — which can never run on PGlite → deadlock. This demo uses `useTransaction: false`; the in-process adapter avoids it entirely because both seams funnel to PGlite's single session. (The socket shim also defaults to `maxConnections: 1`; we raise it — PGlite still serializes, so multiplexing is safe.) |
48 | 53 |
|
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 } })`). |
| 54 | +2. **Extensions are provisioned out-of-band.** pgpm's `cleanSql` deliberately strips `CREATE EXTENSION` (and `BEGIN`/`COMMIT`) from migrations — extensions come from the environment (the `postgres-plus` image / `pgsql-test`'s `installExtensions()`), not migrations. So the demo creates the extension at PGlite bootstrap, and PGlite additionally requires the extension registered in JS at construction (`PGlite.create({ extensions: { vector } })`). |
55 | 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. |
| 56 | +Extension availability — not pgpm — is the functional boundary. Bundled/available: `pg_trgm`, `citext`, `ltree`, `uuid_ossp`, `pgvector`, … Not available (no WASM / no background workers): `pg_cron`, `pg_partman`, the BM25 search ext; PostGIS is experimental. A module needing those simply won't deploy to PGlite. |
60 | 57 |
|
61 | 58 | ## Notes |
62 | 59 |
|
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. |
| 60 | +Standalone on purpose: this directory is **not** part of the pnpm workspace. It has its own `package.json` + `pnpm-lock.yaml` and is installed with `pnpm install --ignore-workspace`, so the demo stays independent of the monorepo build and depends on the *published* `@pgpmjs/core` (it could be repointed at the workspace build to track unreleased engine changes). |
0 commit comments