Skip to content

feat(pglite): pgpm→PGlite plugin — pg-cache + pgsql-client driver seams, @pgpmjs/pglite-adapter, pglite-test#1350

Merged
pyramation merged 12 commits into
mainfrom
feat/pglite-poc
Jul 10, 2026
Merged

feat(pglite): pgpm→PGlite plugin — pg-cache + pgsql-client driver seams, @pgpmjs/pglite-adapter, pglite-test#1350
pyramation merged 12 commits into
mainfrom
feat/pglite-poc

Conversation

@pyramation

@pyramation pyramation commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings PGlite (WASM Postgres) into the pgpm ecosystem as a plugin, keeping all @electric-sql/pglite* deps out of pgpm core and out of pgsql-client/pgsql-test — mirroring the drizzle-orm-test/supabase-test precedent. Nobody using pgpm/pgsql-test is affected; the default path is byte-identical.

The stack talks to Postgres through node-pg in two places, so there are two independent seams (both default-preserving), and the PGlite deps live only in two new leaf packages:

pg-cache.getPgPool ──registerPgPoolFactory──▶ QueryablePool   ┐
pgsql-client.PgClient ─registerPgClientFactory▶ QueryablePgClient ┘  default = pg.Pool / new pg.Client
                                        ▲
        @pgpmjs/pglite-adapter ─ registerPglite() ─ one in-process PGlite session backs both
                                        ▲
                 pglite-test ─ drop-in getConnections() (no server / createdb / psql)

What's here (in rollout order)

1. Socket e2e + CI (poc/pglite/, .github/workflows/pglite.yaml, job pglite-socket-e2e) — the unmodified pgpm engine does deploy → verify → pgvector round-trip → revert against PGlite over a pg-gateway socket shim, no Postgres service container. Standalone (own lockfile, outside the pnpm workspace). Kept as a wire-protocol demonstration distinct from the in-process path below.

2. pg-cache driver seamgetPgPool routes pool construction through a registrable factory; default unchanged. Factory return broadened so an adapter can return any QueryablePool (a real pg.Pool structurally satisfies it). 19 unit tests.

3. @pgpmjs/pglite-adapter (new — one of only two packages importing @electric-sql/pglite) — registers an in-process PGlite instance as the pg-cache factory, so the unmodified pgpm engine deploys into PGlite with no socket, no server:

const { db, close } = await registerPglite();       // creates PGlite, registers pool factory
const migrate = new PgpmMigrate({ database: 'postgres' });
await migrate.deploy({ modulePath });               // useTransaction:true (default)
await close();

The pool maps node-pg's two protocols onto PGlite: parameterized → db.query, parameterless/multi-statement (pgpm bootstrap) → db.exec. 6 tests.

4. pgsql-client client-factory seam — mirror of the pg-cache pool seam, because pgsql-client/pgsql-test build pg.Clients directly (not via getPgPool):

// pgsql-client/src/driver.ts  (no pglite import)
export type PgClientFactory = (config) => Client | QueryablePgClient; // { connect, query, end }
export const registerPgClientFactory = (f?) => { activeFactory = f; };
// PgClient ctor:
this.client = (getActivePgClientFactory() ?? defaultPgClientFactory)(config) as Client;

Default = new pg.Client(...), fully backward-compatible. 4 unit tests.

Publish readiness: @electric-sql/pglite is a peerDependency of both new packages (pinned devDependency for the monorepo build/tests) so consumers pin the version and avoid duplicate installs. pglite-test.getConnections restores the previous client factory on teardown and unwinds the seams if setup throws (no cross-suite leakage). Both packages carry CHANGELOG.md and publishConfig.

5. pglite-test (new — the second pglite-importing package) — a drop-in getConnections() like drizzle-orm-test/supabase-test, backed by an in-process PGlite instance. Composes both seams (registerPglite + registerPgClientFactory) so pg and db share one PGlite session:

const { pg, db, teardown } = await getConnections(
  { pglite: { extensionSql: ['CREATE ROLE authenticated;'] } },
  [seed.pgpm(moduleDir)]                // deploys via getPgPool → PGlite
);

Because PGlite is a single session, transaction control is ref-counted (SharedTxn) so the standard two-client beforeEach()/afterEach() savepoint harness emits exactly one BEGIN/SAVEPOINT/ROLLBACK/COMMIT per test (single-client db-only also works). No PGlite-aware DbAdmin subclass was needed — the instance is the database; extension/role bootstrap runs via pglite.extensionSql. 5 tests: deploy, per-test isolation, and RLS role + JWT-claim switching incl. WITH CHECK.

Key findings

  • No core transaction fix needed for the in-process path. The socket PoC required useTransaction:false (a txn on one TCP session + isDeployed() on another deadlocks a single-connection backend). In-process, both seams funnel to PGlite's single session, so BEGIN/isDeployed()/COMMIT interleave safely. Deferred as optional — only relevant to a hypothetical multi-connection single-writer backend.
  • pgsql-test bypasses the pg-cache seam, which is exactly why the client-factory seam (part 4) exists: PgTestClient extends pgsql-client's PgClient (new Client()) and never calls getPgPool.

Tests / CI

All 38 checks green, including the new no-services unit-tests (pglite, postgres/pglite-adapter postgres/pglite-test) job and Socket Security for @electric-sql/pglite@0.5.4.

Scope: no pgpm core source changed. Both seams are additive and default-preserving; all @electric-sql/pglite* code lives in @pgpmjs/pglite-adapter and pglite-test.

Not in this PR (follow-ups)

  • Fuller pglite-test-suite wiring + getConnections manager/admin parity.
  • Optional core transaction-client fix (only for a multi-connection single-writer backend).
  • pgpm init --pglite scaffolder (CLI-layer convenience; no PGlite dep in core).
  • Browser-target validation (pgsql-parser is libpg_query WASM → two WASM modules load in-browser; fine in Node/CI, untested in-browser).

Link to Devin session: https://app.devin.ai/sessions/424b76f81a60499493ac946b57136ad9
Requested by: @pyramation

@pyramation pyramation self-assigned this Jul 10, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@socket-security

socket-security Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​electric-sql/​pglite-pgvector@​0.0.5641006594100
Added@​electric-sql/​pglite@​0.5.4931009998100
Added@​electric-sql/​pglite-socket@​0.2.79910010098100

View full report

@devin-ai-integration devin-ai-integration Bot changed the title poc(pglite): deploy pgpm migrations into PGlite in CI (no Postgres service) poc(pglite): pgpm→PGlite PoC in CI + pluggable pg-cache driver seam (step 1) Jul 10, 2026
Registers an in-process PGlite instance as the pg-cache pool factory so the
unmodified pgpm engine deploys/verifies/reverts migrations into PGlite (WASM
Postgres) with no server and no socket. All @electric-sql/pglite deps live in
this adapter; core stays PGlite-free.

- broaden pg-cache PgPoolFactory to return pg.Pool | QueryablePool (pg.Pool
  still satisfies it; default path unchanged)
- 6 tests: deploy -> verify -> data -> revert into in-process PGlite with the
  default useTransaction:true (single session, no cross-connection deadlock)
- run in the no-services unit-tests CI tier
@devin-ai-integration devin-ai-integration Bot changed the title poc(pglite): pgpm→PGlite PoC in CI + pluggable pg-cache driver seam (step 1) feat(pglite): pgpm→PGlite plugin — pg-cache driver seam (step 1) + @pgpmjs/pglite-adapter (step 3) Jul 10, 2026
- pgsql-client: add registerPgClientFactory/defaultPgClientFactory seam
  (mirror of pg-cache pool seam); PgClient routes its underlying client
  through it. Default = new pg.Client, fully backward-compatible.
- pglite-adapter: extract shared query runner; add createPgliteClient so
  the same in-process session backs both pool and client seams.
- pglite-test: new drop-in getConnections backed by in-process PGlite
  (no server/createdb/psql). SharedTxn ref-counts transaction control so
  the standard two-client beforeEach/afterEach savepoint harness works
  over one PGlite session. Seeds via seed.pgpm(). 5 tests: deploy, per-test
  isolation, RLS role/JWT switching incl. WITH CHECK.
- CI: add postgres/pglite-test to the no-services unit-tests (pglite) batch.
@devin-ai-integration devin-ai-integration Bot changed the title feat(pglite): pgpm→PGlite plugin — pg-cache driver seam (step 1) + @pgpmjs/pglite-adapter (step 3) feat(pglite): pgpm→PGlite plugin — driver seams (pg-cache + pgsql-client) + @pgpmjs/pglite-adapter + pglite-test Jul 10, 2026
- Make @electric-sql/pglite a peerDependency of @pgpmjs/pglite-adapter and
  pglite-test (kept as a pinned devDependency for the monorepo build/tests),
  so consumers pin the PGlite version and avoid duplicate installs.
- Harden pglite-test getConnections: restore the previous client factory on
  teardown (instead of resetting to default) and unwind seams if setup throws.
- Rename workflow pglite-poc.yaml -> pglite.yaml (job pglite-socket-e2e); it is
  the standalone wire-protocol e2e demo. The in-process path already runs in the
  no-services unit-tests (pglite) job in run-tests.yaml.
- Add CHANGELOG.md stubs for both new packages.
@devin-ai-integration devin-ai-integration Bot changed the title feat(pglite): pgpm→PGlite plugin — driver seams (pg-cache + pgsql-client) + @pgpmjs/pglite-adapter + pglite-test feat(pglite): pgpm→PGlite plugin — pg-cache + pgsql-client driver seams, @pgpmjs/pglite-adapter, pglite-test Jul 10, 2026
- Drop package-lock.json; add pnpm-lock.yaml (installed with --ignore-workspace
  so it stays standalone on the published @pgpmjs/core, not the workspace build).
- Rename run.mjs -> run.js (package.json already has type:module).
- Workflow pglite.yaml: pnpm/action-setup + pnpm install --ignore-workspace / pnpm start.
- Rewrite poc/pglite/README.md in the sibling test-framework style (badges, structure).
Repo convention avoids package type:module. All three @electric-sql/pglite*
packages expose a require entry, so run.js uses CommonJS require + async main().
- Move poc/pglite -> examples/pglite-socket (no more 'poc').
- Make it a proper workspace package: workspace deps (@pgpmjs/core, pg-cache),
  jest.config.js + tsconfig.json + CHANGELOG; drop standalone pnpm-lock.yaml and
  the --ignore-workspace install.
- Convert run.js -> __tests__/socket.e2e.test.ts (jest, real expect assertions);
  teardownPgPools() in afterAll so no idle-pool noise.
- Delete bespoke .github/workflows/pglite.yaml; run the socket e2e in the existing
  no-services unit-tests (pglite) batch in run-tests.yaml alongside the adapter + pglite-test.
…file delta

Exact pg@8.22.0 in the example forced a repo-wide pg 8.21.0->8.22.0 bump in the
lockfile, which drifted the graphile/postgraphile plan-step resolution and broke
graphile-bulk-mutations. Match the repo's ^8.21.0 and keep the lockfile delta to
just the new example package.
@pyramation pyramation merged commit e443efb into main Jul 10, 2026
38 checks passed
@pyramation pyramation deleted the feat/pglite-poc branch July 10, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant