Skip to content

Commit 015de9b

Browse files
committed
poc(pglite): deploy pgpm migrations into PGlite in CI (no Postgres service)
1 parent a3d8738 commit 015de9b

18 files changed

Lines changed: 1791 additions & 0 deletions

File tree

.github/workflows/pglite-poc.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: PGlite PoC
2+
3+
# Proof of concept: the pgpm migrate engine deploying real migrations into PGlite
4+
# (WASM Postgres). Deliberately has NO `services:` block — PGlite runs in-process,
5+
# so no Postgres service container is needed.
6+
7+
on:
8+
push:
9+
branches: [main, v1]
10+
paths:
11+
- 'poc/pglite/**'
12+
- '.github/workflows/pglite-poc.yaml'
13+
pull_request:
14+
branches: [main, v1]
15+
paths:
16+
- 'poc/pglite/**'
17+
- '.github/workflows/pglite-poc.yaml'
18+
workflow_dispatch:
19+
20+
jobs:
21+
pglite-poc:
22+
if: github.event.pull_request.draft != true
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 10
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: actions/setup-node@v4
28+
with:
29+
node-version: '22'
30+
- name: Install (standalone, not the monorepo)
31+
working-directory: poc/pglite
32+
run: npm ci
33+
- name: Deploy pgpm migrations into PGlite (no Postgres service)
34+
working-directory: poc/pglite
35+
run: npm start

poc/pglite/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- Deploy test-simple:embedding to pg
2+
-- requires: table
3+
4+
BEGIN;
5+
6+
CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public;
7+
ALTER TABLE test_app.users ADD COLUMN embedding public.vector(3);
8+
9+
COMMIT;

poc/pglite/module/deploy/index.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- Deploy test-simple:index to pg
2+
-- requires: table
3+
4+
BEGIN;
5+
6+
CREATE INDEX idx_users_email ON test_app.users(email);
7+
CREATE INDEX idx_users_created_at ON test_app.users(created_at);
8+
9+
COMMIT;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Deploy test-simple:schema to pg
2+
3+
BEGIN;
4+
5+
CREATE SCHEMA test_app;
6+
7+
COMMIT;

poc/pglite/module/deploy/table.sql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- Deploy test-simple:table to pg
2+
-- requires: schema
3+
4+
BEGIN;
5+
6+
CREATE TABLE test_app.users (
7+
id SERIAL PRIMARY KEY,
8+
name TEXT NOT NULL,
9+
email TEXT UNIQUE NOT NULL,
10+
created_at TIMESTAMPTZ DEFAULT NOW()
11+
);
12+
13+
COMMIT;

poc/pglite/module/pgpm.plan

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
%syntax-version=1.0.0
2+
%project=test-simple
3+
%uri=https://github.com/test/simple
4+
5+
schema 2024-01-01T00:00:00Z test <test@example.com> # Create schema
6+
table [schema] 2024-01-02T00:00:00Z test <test@example.com> # Create table
7+
index [table] 2024-01-03T00:00:00Z test <test@example.com> # Create index
8+
embedding [table] 2024-01-04T00:00:00Z test <test@example.com> # Add pgvector column
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Revert test-simple:embedding from pg
2+
BEGIN;
3+
ALTER TABLE test_app.users DROP COLUMN embedding;
4+
COMMIT;

poc/pglite/module/revert/index.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Revert test-simple:index from pg
2+
3+
BEGIN;
4+
5+
DROP INDEX test_app.idx_users_email;
6+
DROP INDEX test_app.idx_users_created_at;
7+
8+
COMMIT;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Revert test-simple:schema from pg
2+
3+
BEGIN;
4+
5+
DROP SCHEMA test_app CASCADE;
6+
7+
COMMIT;

0 commit comments

Comments
 (0)