refactor(db): Prisma → Drizzle — the fleet has one ORM - #154
Merged
Conversation
…aped live DB drizzle-kit pull against a scratch Postgres built by the 29 Prisma migrations, then: mode 'date', $defaultFn(createId) on the 43 cuid ids, $defaultFn/$onUpdateFn on the 29 @updatedat tables, bytea customType for the two Bytes columns, ARRAY[]::TEXT[] defaults restored (drizzle-kit misparses them as ["RAY"]), and per-column .op() specifiers stripped (drizzle-kit scrambles them positionally in multi-column indexes; they were all Postgres defaults anyway). Parity proof: Prisma chain -> db A, drizzle 0000 -> db B, normalized pg_dump --schema-only diff = EMPTY (1486 DDL lines each; tables, enums, indexes, FKs, uniques, the Message_one_author CHECK — names included). Relations mirror Prisma's field names so include:->with: reads identically; types.ts reproduces Prisma's per-enum runtime objects and row types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
…s + CI + docs) 116 src files with live call sites and 50 type-import files converted: prisma.x.find* -> db.query.x.*, create/update/delete -> insert/update/delete builders, upserts -> onConflictDoUpdate, $transaction -> db.transaction, $queryRaw -> db.execute(sql), Prisma.sql/join/empty -> drizzle sql, P2002 catches -> isUniqueViolation (SQLSTATE 23505, DrizzleQueryError-aware). Nullable TEXT[] columns (DB truth Prisma's types papered over) are normalised at the UI boundary. Type-level FK cycles (Placement<->Incident, HouseRule<->Proposal) broken with PgTableExtraConfigValue[] return annotations. CI: prisma generate steps dropped, schema init = npm run db:migrate, seeds = db:seed/db:seed:admin. deploy-demo.sh migrates via ledgered psql loop (same _deploy_schema_history the fleet applier keeps). Docs updated. Tests and prisma/seed*.ts still reference the old client - next commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
…eeds; delete Prisma
Tests (62 files): jest.mock('@/lib/db') now fakes `db` (query API +
chainable insert/update/delete builders + transactions + execute) instead
of the Prisma model map, with jest.requireActual keeping tables/enums/
helpers real. Assertions moved from `{data}`/`{where}` object equality to
`.values()` payloads and real drizzle where-expressions - discriminating
power kept, nothing weakened to anything(), no test deleted or skipped.
New shared helper src/test-utils/drizzle-where.ts (eqParts/whereParts/
sqlText) keeps mock dispatch DRY. jest transformIgnorePatterns admits the
ESM-only @paralleldrive/cuid2 + @noble/hashes.
Seeds: prisma/seed*.ts -> scripts/db/ (git mv), converted and PROVEN by
execution against a scratch Postgres: db:migrate + db:seed + db:seed:admin
all exit 0, 25 residents / 18 placements / live compatibility scores, and
a second run over the populated DB confirms the FK deletion order and
idempotent governance path.
Prisma is now GONE: prisma/ (schema + 30 migrations) deleted, @prisma/
client + prisma removed from package.json, no remaining import outside
historical comments. The guard tests that used to regex schema.prisma now
read the pgEnum objects at runtime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
…ers, 0001 Master's StaffUnit/SiteAccess feature landed mid-migration in Prisma terms; this ports its data layer 1:1: siteAccess pgEnum + User column + StaffUnit table (pinned Prisma constraint names), unitAccess/staffAccess relations, and drizzle/0001_staff_site_access.sql whose DDL matches the Prisma migration statement for statement. Scratch parity re-proven with 0001 included: normalized pg_dump diff of the full Prisma chain (30 migrations) vs drizzle 0000+0001 is EMPTY. site-access.ts now emits drizzle where-fragments: inArray for unit/ housingUnitId scopes (callers name their table's column), the residents- with-an-ACTIVE-placement rule as an inArray subquery built on a standalone QueryBuilder (constructing a filter must not touch the lazy client - jest and next build call it without DATABASE_URL), and `sql\`false\`` for the assigned-nowhere case (drizzle's inArray throws on []). getCurrentUser carries siteAccess + assignedUnitIds off the row via `with: unitAccess`. The boards pass the fragments through `and(filter ?? undefined, ...)` so an ALL_UNITS viewer's query is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
… 0002 Same drill as #152: InterpreterNeed pgEnum + Resident.interpreterNeed (default NONE) + Resident_interpreterNeed_idx, drizzle/0002 matching the Prisma migration statement for statement, InterpreterNeed runtime enum object in types.ts, and the three new imports repointed at @/lib/db. Scratch parity re-proven: full Prisma chain (31) vs drizzle 0000-0002, normalized pg_dump diff EMPTY. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
catomean
force-pushed
the
refactor/prisma-to-drizzle
branch
from
September 2, 2026 09:41
416714c to
12f4004
Compare
This was referenced Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Final repo of the stack-uniformity migration (George: "Drizzle everywhere") — after biaslens#26, reparaturbonus-zh#130 and solon#136. This was the largest of the four: 46 tables / 69 enums / 29 migrations at branch time, ~500 Prisma call sites in 116 src files, 62 test suites mocking the Prisma client — and, uniquely, a mid-flight master feature (#152) that had to be ported on the way through.
Recipe deltas vs reparaturbonus#130
drizzle-kit pull— exact camelCase columns,"AgeRange"-style enum names, TIMESTAMP(3), and every Prisma constraint/index name pinned by construction. Manual passes on top:mode: 'date',$defaultFn(createId)on exactly the 43 cuid ids,$defaultFn/$onUpdateFnon exactly the 29@updatedAttables (both sets diffed against schema.prisma), abyteacustomType for the twoBytescolumns,ARRAY[]::TEXT[]defaults restored (drizzle-kit misparses them as["RAY"]), and per-column.op()specifiers stripped (drizzle-kit scrambles them positionally in multi-column indexes; all were Postgres defaults).(table): PgTableExtraConfigValue[] =>return annotations —AnyPgColumncasts alone don't cut the inference loop.TEXT[]truth: Prisma typed scalar listsstring[]; the columns are nullable. The UI contract stays non-null via?? []at the projection boundary (toResidentUiSummary,ResidentSummary).photo,expensesPaid,taskRequestsReceived, …,relationName= Prisma's@relationnames), so everyinclude:became the same-shapedwith:.types.tsreproduces what@prisma/clientexported: row types AND per-enum runtime objects (StaffRole.ADMIN), derived from the pgEnums.isUniqueViolation()replacesPrismaClientKnownRequestError/P2002 (SQLSTATE 23505, unwraps DrizzleQueryErrorcause).placements: { some: … }) is aninArraysubquery built on a standaloneQueryBuilderso constructing a filter never touches the lazy client (jest + next build run without DATABASE_URL); empty assignment rendersfalse(drizzleinArraythrows on[]).Parity — proven, not asserted
Scratch Postgres 16: full Prisma chain (30 migrations) → db A;
drizzle/0000+drizzle/0001viadrizzle-kit migrate→ db B; normalizedpg_dump --schema-only --no-owner --no-privilegesdiff (bookkeeping tables excluded): empty — 1486+ DDL lines, all tables, enums, indexes, FKs, uniques and theMessage_one_authorCHECK identical including names.The live
aoz_wohnendump was also diffed against db A: semantically identical; only column/enum-value ordering differs (columns added by later ALTERs sit at the end on live;LOW_SATISFACTIONwas enum-appended) plus live'sUser_code_keybeing a unique index rather than a unique constraint — equivalent objects, append-history artifacts, no action needed.Verification
npm run verifygreen: format:check + lint + typecheck + 204/204 suites, 3584 passed / 8 skipped (3592). Baseline before migration: 202 suites / 3563+8; the deltas are master's two new suites (A coach should not be the last to know their client's house is in trouble #150–152) and dynamically discoveredit.eachrows (a source-scanning gate now finds 7 insert sites instead of 5) — no hand-written test was added, deleted, skipped or weakened.npm run buildgreen (no codegen step left;buildis justnext build).db:migrate+db:seed+db:seed:adminall exit 0 (25 residents, 18 placements, live-computed compatibility scores), and a second run over the populated DB confirms FK deletion order + idempotent governance path.prisma generatesteps; e2e schema init isnpm run db:migrate, seeds aredb:seed/db:seed:admin.Live-DB cutover — non-destructive by construction, already armed
Deploys run fleetcrown's
apply-schema.sh, which probes./drizzlefirst. Both migrations are marked applied, never run, in both ledgers, on the box, before this merges — so the merge deploy's schema step logs "up to date" and ships code only:aoz_wohnen(live, 19 residents):_deploy_schema_history= {0000, 0001};drizzle.__drizzle_migrationscarries both sha256s + journal timestamps. The Which places a staff member is responsible for #152 Prisma migration had already reached this DB via master's deploy, so its schema is the 0000+0001 shape (verified above).aoz_demo(dormant demo instance — service disabled since 2026-08-26): was 9 migrations stale; the missing, verified-additive Prisma migrations were applied by hand first (15 residents before and after), then the same dual-ledger baseline.scripts/deploy-demo.shnow appliesdrizzle/*.sqlthrough the same_deploy_schema_historyledger instead ofnpx prisma migrate deploy.DROP TABLE "_prisma_migrations"(step 3, run separately).Row counts for every table were recorded before the cutover and will be re-verified after the deploy (nothing in this PR writes to the live DB; the deploy applies no schema).
Prisma is gone
prisma/(schema + 30 migrations),@prisma/client,prisma, thepackage.json#prismablock, all CI generate steps, and every import.grep -rni prismain src/scripts now hits only historical comments explaining why code is shaped the way it is. Guard tests that used to regexschema.prismaread the pgEnum objects at runtime instead. Docs (CLAUDE.md, README, INFRASTRUCTURE.md, demo guides) updated. Closes the door on dependabot #126/#127.🤖 Generated with Claude Code
https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn