Skip to content

feat(typewriter): add an in-memory PGlite database transport - #113

Closed
kyscott18 wants to merge 1 commit into
mainfrom
feat/db-pglite
Closed

kyscott18 wants to merge 1 commit into
mainfrom
feat/db-pglite

Conversation

@kyscott18

Copy link
Copy Markdown
Collaborator

Second of four stacked PRs, on #112: db transport → PGlite transport → sim DB as a transport with injected errors → PGlite for the sim.

  • DatabaseOptions takes { kind: "pglite" }, an in-memory PGlite that lasts as long as the process. { url, ... } is unchanged (kind is optional for Postgres), so existing configs keep working.
  • makePgliteTransport (@electric-sql/pglite 0.5.8, the version abipg already pins): a real PGlite transaction, no retries (notApplied is never set: there is no connection to lose), and a no-op advisory lock (only this process can reach an in-memory database).
  • runTransaction is the Effect-to-promise bridge, taken out of the Postgres transaction so both transports share it; the commit-status check stays a Postgres-only hook. A failed statement still fails the transaction even if f recovers. On PGlite, a COMMIT after a failed statement silently rolls back and resolves.
  • Drizzle is now the driver-neutral PgDatabase<PgQueryResultHKT>. selectMaxExecutionIndex selects from the subquery (max(max)::text) instead of reading db.execute, which returns { rows } on PGlite but an array on Bun SQL.

Transaction guard. PGlite has one session. Its query/transaction already queue behind an open transaction, so transactions from separate fibers can't overlap. The real hazard is a statement on db instead of tx inside f. On PGlite that waits on its own transaction forever; on Postgres it silently runs outside the transaction. makeDbClient runs f with an InTransaction reference, and query/transaction die when it is set, including in fibers that f forks.

Known issue, no workaround yet. PGlite reads a text value that starts with U+FEFF (a BOM) without it. Its protocol reader decodes each field with new TextDecoder("utf-8"), which strips a leading BOM by default; the stored bytes are correct. Still the case in 0.5.14. Documented on makePgliteTransport. The runtime never reads text columns back, and the sim stores no strings since #111, so nothing here is affected. db-query.test.ts stays on Postgres: pointed at PGlite, its BOM case fails and every other string passes.

Checked:

🤖 Generated with Claude Code

`DatabaseOptions` takes `{ kind: "pglite" }`; `{ url, ... }` is unchanged.
`makePgliteTransport` runs an in-memory PGlite: it shares the transaction
bridge with Postgres, has nothing to retry, and needs no advisory lock.

- `runTransaction` is the Effect-to-promise bridge from the Postgres
  transaction, with the commit-status check as a Postgres-only hook.
- `Drizzle` is the driver-neutral `PgDatabase`, and
  `selectMaxExecutionIndex` selects from the subquery instead of reading
  `db.execute`, whose result has a different shape on each driver.
- `makeDbClient` fails when the database is used inside a transaction
  instead of `tx`, including from fibers that `f` forks. That statement
  runs outside the transaction on Postgres, and waits on it forever on
  PGlite, whose one session the transaction holds. Transactions from
  separate fibers queue behind each other on PGlite's own mutex.

PGlite reads a text value that starts with U+FEFF without it: its
protocol reader decodes each field with a `TextDecoder` that strips a
leading BOM. It is documented on `makePgliteTransport`, with no
workaround yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyscott18
kyscott18 marked this pull request as ready for review November 2, 2026 21:22
@kyscott18 kyscott18 closed this Nov 2, 2026
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