Skip to content

refactor(db): migrate ORM from Drizzle to Prisma#108

Open
jee7s wants to merge 1 commit into
mainfrom
chore/migrate-drizzle-to-prisma
Open

refactor(db): migrate ORM from Drizzle to Prisma#108
jee7s wants to merge 1 commit into
mainfrom
chore/migrate-drizzle-to-prisma

Conversation

@jee7s

@jee7s jee7s commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces Drizzle ORM with Prisma 7 across @arbor/db, preserving the public store contract so no consuming package changes: PostgresUrlStore / PostgresConfigStore / PostgresAuditStore (and SQLite equivalents), the createStores() factory, and the UrlEntry / AuditRecord types are all unchanged. The agent, lambda, logger, and mcp-url-fetcher packages are untouched.

Dual-database design (preserved)

The existing "Postgres in prod, SQLite for dev/tests" split is kept. Prisma binds a client to one provider at generate time, so this uses two schemas generating two clients:

Schema Generated client Driver adapter
Postgres (prod) prisma/postgres/schema.prisma generated/postgres @prisma/adapter-pg
SQLite (dev/test) prisma/sqlite/schema.prisma generated/sqlite @prisma/adapter-better-sqlite3

createStores() selects the client by connection string exactly as before. Prisma 7 has no embedded query engine (it uses a WASM query compiler + driver adapters).

SQLite specifics

The zero-migration, schema-on-first-connect behaviour is retained — including :memory:. The better-sqlite3 adapter keeps a single connection, so the bootstrap DDL (run via $executeRawUnsafe) applies to the same connection the client queries. timestampFormat: "iso8601" preserves the ISO-string added_at / created_at contract.

Tooling changes

  • db:generateprisma generate for both schemas. A postinstall hook regenerates the clients on every npm install/npm ci, so CI's non-build jobs (unit, dev) and Docker always have them. Generated clients are gitignored (packages/db/generated/).
  • db:migrateprisma migrate deploy. Postgres migrations now live in prisma/postgres/migrations/; the baseline 20260608000000_init mirrors the previous Drizzle schema.
  • Dockerfile: migrate stage runs prisma migrate deploy; the builder copies schemas before install (for the postinstall generate) and the runtime stage copies the generated clients.
  • Removed deps: drizzle-orm, drizzle-kit, postgres. Added: @prisma/client, prisma, @prisma/adapter-pg, @prisma/adapter-better-sqlite3.

⚠️ Operational note — baseline existing databases once

Existing prod/dev databases already have these tables. Before the first migrate deploy, mark the baseline as applied so Prisma doesn't try to recreate them:

cd packages/db
DATABASE_URL="postgres://..." npx prisma migrate resolve \
  --applied 20260608000000_init --config prisma.config.postgres.ts

Fresh databases need no baselining. Documented in docs/deployment.md.

Verification

  • ✅ Full monorepo build (npm run build)
  • ✅ 255 unit tests (npm run test)
  • ✅ 27 SQLite dev/integration tests (npm run test:dev) — :memory:, file-backed persistence, Boolean/DateTime/ISO roundtrip, autoincrement ids, ordering, null model
  • ✅ Coverage thresholds still met (91% lines / 87% branches)
  • ✅ Lambda bundle: WASM query compiler inlined as base64 (no on-disk .wasm), bundled handler loads, Postgres query path initialises (connection-only failure against a dead DB, i.e. the compiler works)
  • ⚠️ Live Postgres roundtrip not run locally (no Postgres server in the dev env); the query layer is identical to the SQLite path that passes, and CI's Postgres integration job exercises migrate deploy against a real DB

Notes for reviewers

  • Prisma model fields are camelCase mapped to the existing snake_case columns (@map), so the DB schema is unchanged.
  • delete uses deleteMany to keep the no-throw-on-missing semantics Drizzle had.
  • This branch is independent of the ECR→GHCR branch; the only workflow changes here are one-line comment fixes (drizzle-kit migrateprisma migrate deploy).

🤖 Generated with Claude Code

Replaces Drizzle ORM with Prisma 7 across @arbor/db while preserving the
public store contract (PostgresUrlStore/ConfigStore/AuditStore + SQLite
equivalents, createStores factory, and the UrlEntry/AuditRecord types) so no
consuming package changes.

Dual-database support is kept via two Prisma schemas that generate separate
clients (prisma/postgres + prisma/sqlite) into packages/db/generated, selected
at runtime by createStores() as before. Prisma 7 has no embedded query engine,
so connections go through driver adapters: @prisma/adapter-pg for Postgres and
@prisma/adapter-better-sqlite3 for SQLite. The SQLite path keeps its
zero-migration, schema-on-first-connect behaviour (including :memory:) by
bootstrapping the DDL over the adapter's single connection; timestampFormat
"iso8601" preserves the ISO-string timestamp contract.

Tooling:
- db:generate -> prisma generate (both schemas); postinstall regenerates
  clients on every install so CI's non-build jobs and Docker have them.
- db:migrate -> prisma migrate deploy; Postgres migrations live in
  prisma/postgres/migrations (baseline 20260608000000_init mirrors the prior
  Drizzle schema). Existing DBs must baseline once via `prisma migrate resolve`
  (see docs/deployment.md).
- Dockerfile migrate stage now runs prisma migrate deploy; builder copies the
  schemas before install and runtime copies the generated clients.

Generated clients are gitignored. Docs updated to reference Prisma.

Verified: full build, 255 unit tests, 27 SQLite dev/integration tests, and the
Lambda bundle (Prisma WASM query compiler inlined, handler loads, pg query
path initialises). Live Postgres roundtrip is exercised by CI's Postgres job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jee7s jee7s requested review from dkaygithub and thpr June 10, 2026 03:27
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