Skip to content

refactor(db): Prisma → Drizzle — the fleet has one ORM - #26

Merged
github-actions[bot] merged 1 commit into
mainfrom
refactor/prisma-to-drizzle
Sep 1, 2026
Merged

refactor(db): Prisma → Drizzle — the fleet has one ORM#26
github-actions[bot] merged 1 commit into
mainfrom
refactor/prisma-to-drizzle

Conversation

@catomean

@catomean catomean commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Stack-uniformity migration (George, 2026-09-01: "Drizzle everywhere" for fleet-owned Postgres apps). biaslens is the recipe-setter — smallest of the four Prisma repos; reparaturbonus-zh, solon, and aoz-housing follow this pattern.

Call sites (before → after)

Prisma surface was small and is now zero:

Site Before After
src/lib/db/prisma.ts PrismaClient singleton deleted → src/lib/db/client.ts (lazy pg Pool + drizzle(), fleet house pattern from hirnli)
src/app/api/claims/[id]/verify/route.ts prisma.claim.findUnique({ select: { evidence: … } }) db.query.claims.findFirst({ columns: {}, with: { evidence: { columns: { stance: true } } } }) — same shape, same over-fetch discipline
src/lib/domain/claim-verification.ts import type { Evidence } from '@prisma/client' import type { Evidence } from '../db/schema' ($inferSelect)
route.test.ts mocks @/lib/db/prisma / findUnique mocks @/lib/db/client / db.query.claims.findFirst (missing row = undefined, matching Drizzle)

1 query call site rewritten, 1 client module replaced, 1 type import repointed, 1 test mock retargeted. No seed script existed. grep -rni prisma over the tree now hits only the lockfile (drizzle-orm's own optional peers), historical comments in the parity note, and the README history line.

Schema parity — proven, not asserted

The DB was shaped by Prisma's 20260714233936_init; the Drizzle schema matches those tables, it does not reinvent them. Every table, column, index, and FK constraint name is byte-identical ("Outlet", "outletId", "Article_outletId_idx", "Article_outletId_fkey", cascade rules included). Client-side behavior is preserved too: cuid ids minted in the app ($defaultFn(createId) — Prisma's @default(cuid()) was also client-side, the columns carry no DB default) and EditorialDna.updatedAt set via $onUpdate, as @updatedAt was.

Evidence: on a scratch Postgres 16 (docker), applied Prisma's original migration.sql to db A and drizzle/0000 (via npm run db:migrate) to db B, then diffed normalized pg_dump --schema-only output. The only difference is Drizzle's own bookkeeping (CREATE SCHEMA drizzle; + its __drizzle_migrations table vs Prisma's _prisma_migrations); the DEFAULT CURRENT_TIMESTAMP vs DEFAULT now() rendering is the same Postgres function. All 7 application tables identical.

Live smoke (same scratch DB, next build output served): GET /api/claims/c1/verify over seeded evidence returned {"status":"supported","confidence":0.667,"inputs":{"supports":2,"contradicts":1,"total":3}}; missing id returned the structured 404.

Existing-DB baseline

drizzle/0000 recreates the schema for fresh databases. A database already shaped by Prisma must not run it — mark it applied instead (Drizzle journals in schema drizzle, table __drizzle_migrations):

CREATE SCHEMA IF NOT EXISTS "drizzle";
CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
  id SERIAL PRIMARY KEY, hash text NOT NULL, created_at bigint
);
-- hash = sha256 of drizzle/0000_nosy_thor_girl.sql; created_at = the "when" ms value in drizzle/meta/_journal.json
INSERT INTO "drizzle"."__drizzle_migrations" (hash, created_at)
  VALUES ('f97125ec3ff5eaa4c4a4023229f18e11e14dfd2326ebd5358acf60fcd7f98b9a', 1788298914447);
DROP TABLE IF EXISTS "_prisma_migrations";

biaslens itself is CI-only (no deploy pipeline, CI provisions no DB — tests mock the client, next build is hermetic), so no live database needed baselining here. The three follow-up repos with real DBs will need the baseline step above wired into their deploy path.

Verification

npm run verify (format:check + lint + typecheck + 17/17 tests) and npm run build green locally. CI's prisma generate step is deleted — Drizzle has no codegen, types flow from src/lib/db/schema.ts at typecheck time. Note: npm audit reports 4 moderate advisories inside drizzle-kit's bundled dev-only toolchain (@esbuild-kit/*) — devDependency, same version the fleet (hirnli) already runs, no audit gate in CI.

The recipe (for reparaturbonus-zh, solon, aoz-housing)

  1. Survey: read prisma/schema.prisma; grep @prisma/client + PrismaClient usage; list every query call site and the seed script.
  2. Schema translation: write src/lib/db/schema.ts matching the existing tables (Prisma's migrations are the ground truth): exact table/column names (Prisma quotes PascalCase/camelCase — keep them), timestamp(N)/doublePrecision/jsonb per the SQL, uniqueIndex/index/foreignKey({name}) with Prisma's generated names, cascade rules. Reproduce client-side behaviors: @default(cuid())$defaultFn(() => createId()) (@paralleldrive/cuid2), @updatedAt$defaultFn + $onUpdate. Add relations() mirrors and $inferSelect type exports.
  3. Client: src/lib/db/client.ts — lazy drizzle(new Pool(...), { schema }) proxy (build/CI-hermetic, hot-reload-safe), the single DB door.
  4. Config + migration: drizzle.config.ts (schema → ./src/lib/db/schema.ts, out → ./drizzle); npx drizzle-kit generate for the 0000 baseline.
  5. Prove parity: scratch Postgres, Prisma SQL → db A, drizzle-kit migrate → db B, diff normalized pg_dump --schema-only. Only bookkeeping tables may differ.
  6. Rewrite call sites: findUnique/findFirstdb.query.<table>.findFirst, selectcolumns/with, missing row is undefined not null; $transactiondb.transaction; port the seed script; retarget test mocks.
  7. Delete the old stack: prisma/ dir, @prisma/client + prisma deps, prisma npm scripts, prisma generate CI steps, .env/docs/gitignore mentions. Done only when grep -rni prisma is clean (lockfile optional-peers aside).
  8. Existing DBs: baseline-insert into drizzle.__drizzle_migrations (SQL above) instead of running 0000; drop _prisma_migrations. Wire into the repo's deploy path.
  9. Verify: install, verify script, build, live smoke against the scratch DB through the real HTTP routes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn

Stack-uniformity migration (recipe-setter for reparaturbonus-zh, solon,
aoz-housing). src/lib/db/schema.ts is now the SSOT for domain types;
drizzle/0000 recreates byte-identical tables (proven by normalized
pg_dump diff against Prisma's init migration on a scratch Postgres).
Existing DBs need only a baseline mark — no structural change.

- schema.ts: 7 tables, exact Prisma names (tables, columns, indexes,
  FK constraints incl. cascade rules); cuid ids and updatedAt minted
  client-side, exactly as Prisma did
- client.ts: lazy pg Pool singleton (hermetic build/CI, hot-reload safe)
- route rewritten prisma.claim.findUnique -> db.query.claims.findFirst
- CI: prisma-generate step deleted (Drizzle has no codegen)
- prisma/ dir, @prisma/client + prisma deps, prisma scripts: gone

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
@github-actions
github-actions Bot merged commit 80781e1 into main Sep 1, 2026
1 check passed
@github-actions
github-actions Bot deleted the refactor/prisma-to-drizzle branch September 1, 2026 21:49
github-actions Bot pushed a commit to bitbaum/reparaturbonus-zh that referenced this pull request Sep 1, 2026
Stack-uniformity migration (George, 2026-09-01: "Drizzle everywhere"),
following the recipe proven in bitbaum/biaslens#26.

- src/lib/db/schema.ts: exact-parity schema (Prisma's table/column/enum/
  constraint names pinned; cuid2 ids + $onUpdateFn replacing client-side
  @default(cuid()) / @updatedat)
- src/lib/db/index.ts: lazy pg Pool + drizzle() singleton (build-hermetic)
- 11 query call sites rewritten (auth, register, admin stats/activity,
  bonus-codes ×3, shops, test, sitemap, shop metadata layout)
- NextAuth PrismaAdapter dropped: unused under credentials + JWT strategy
- seed/upsert scripts ported to scripts/db/; prisma/, prisma.config.ts,
  src/generated/, all 4 prisma deps and CI prisma steps deleted
- drizzle/0000 baseline: pg_dump-diff-proven identical to Prisma's 0_init
  on scratch Postgres 16 (only ORM bookkeeping tables differ)


Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn

Co-authored-by: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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