Migrate from Prisma to Drizzle ORM: A Step-by-Step Guide with SchemaForge
Teams are moving from Prisma to Drizzle ORM in growing numbers. The reasons are clear: Drizzle gives you SQL-first type safety without a Rust engine running in the background, generates zero-overhead TypeScript queries, and integrates natively with your existing database tooling. But the migration itself — rewriting every Prisma model as a Drizzle schema — is tedious, error-prone, and can take days for a medium-sized project.
SchemaForge automates the conversion. One command turns your Prisma schema into Drizzle TypeScript code: models, relations, enums, indexes, and default values — all preserved.
Try it now
pip install git+https://github.com/Coding-Dev-Tools/schemaforge.git · Convert your Prisma schema to Drizzle in seconds
View on GitHub →In This Article
- Why Teams Are Moving from Prisma to Drizzle
- The Pain of Manual Migration
- How SchemaForge Automates the Conversion
- Step 1: Install SchemaForge
- Step 2: Convert Your Prisma Schema
- Step 3: Review the Drizzle Output
- Step 4: Customize Type Mappings
- Step 5: Verify with Diff
- Step 6: Generate Alembic Migrations
- Roundtrip Verification
- Handling Edge Cases
- Get Started
Why Teams Are Moving from Prisma to Drizzle
The Prisma-to-Drizzle migration trend isn't hype — it's driven by concrete technical advantages:
| Prisma | Drizzle | |
|---|---|---|
| Query approach | Prisma Client (auto-generated, Rust engine) | SQL-like TypeScript queries (zero runtime overhead) |
| Engine | Rust binary (~50MB), separate process | No engine — pure JavaScript/TypeScript |
| Type safety | Generated client, good but indirect | Direct TypeScript types from schema definitions |
| SQL control | Abstracted — you write Prisma queries, not SQL | SQL-first — you see and control the SQL |
| Bundle size | Heavy (engine + client) | Lightweight — tree-shakeable |
| Edge runtime | Limited support (requires Prisma Accelerate) | Full edge compatibility |
| Migrations | Prisma Migrate (declarative, opinionated) | drizzle-kit (SQL-based, flexible) |
For teams that want SQL-level control with TypeScript-level type safety, Drizzle is the better fit. The only barrier is the migration cost.
The Pain of Manual Migration
Consider a typical Prisma schema with 15 models, 30 relations, 8 enums, and 20 indexes. Manually converting each model to Drizzle involves:
- Rewriting every
modelblock as apgTable()(ormysqlTable()) definition - Converting Prisma field types to Drizzle column types (e.g.,
String @db.Uuid→uuid()) - Translating relation blocks to Drizzle's
relations()API - Converting
@@indexand@@uniqueto Drizzle's.index()and.unique() - Handling enum definitions (
enum→pgEnum()) - Mapping default values (
@default(autoincrement())→serial().primaryKey())
For 15 models, that's hours of careful, mechanical work — and every manual conversion is an opportunity for a bug. A missed relation, a wrong type mapping, or a forgotten index can cause silent data corruption.
How SchemaForge Automates the Conversion
SchemaForge uses a shared Internal Representation (IR) — every supported format (SQL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy, JSON Schema, GraphQL, EF Core, Scala) converts to and from this common schema model. This means:
- Bidirectional conversion — Prisma → Drizzle, Drizzle → Prisma, or any of the other 108 direction pairs
- Zero-loss roundtripping —
prisma → drizzle → prismaproduces the same schema you started with - Extensibility — adding a new format requires only a parser and a generator, no pairwise converters
For the Prisma-to-Drizzle path specifically, SchemaForge's IR captures every schema element: tables, columns, types, relations, indexes, enums, constraints, and default values. The Drizzle generator then produces idiomatic TypeScript code using the correct Drizzle column constructors.
Step 1: Install SchemaForge
# Install SchemaForge
pip install git+https://github.com/Coding-Dev-Tools/schemaforge.git
# Verify installation
schemaforge --help
Requires Python 3.10+. Works on macOS, Linux, and Windows.
Step 2: Convert Your Prisma Schema
Let's start with a realistic Prisma schema for a SaaS application:
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
ADMIN
EDITOR
VIEWER
}
enum SubscriptionTier {
FREE
PRO
ENTERPRISE
}
model User {
id String @id @default(uuid())
email String @unique
name String?
role Role @default(VIEWER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
subscription Subscription?
@@index([email])
@@map("users")
}
model Post {
id String @id @default(uuid())
title String
content String?
published Boolean @default(false)
createdAt DateTime @default(now())
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags Tag[]
@@index([authorId])
@@index([published, createdAt])
@@map("posts")
}
model Tag {
id String @id @default(uuid())
name String @unique
posts Post[]
@@map("tags")
}
model Subscription {
id String @id @default(uuid())
tier SubscriptionTier @default(FREE)
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("subscriptions")
}
Now convert it:
schemaforge convert --from prisma --to drizzle --input schema.prisma
That's it. SchemaForge reads the Prisma schema, builds the IR, and generates Drizzle TypeScript code.
Step 3: Review the Drizzle Output
SchemaForge generates idiomatic Drizzle schema code. Here's what the output looks like:
import { pgTable, uuid, varchar, text, boolean, timestamp, pgEnum } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Enums
export const roleEnum = pgEnum('Role', ['ADMIN', 'EDITOR', 'VIEWER']);
export const subscriptionTierEnum = pgEnum('SubscriptionTier', ['FREE', 'PRO', 'ENTERPRISE']);
// Tables
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: varchar('email').notNull().unique(),
name: varchar('name'),
role: roleEnum('role').default('VIEWER').notNull(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').notNull(),
}, (table) => ({
emailIdx: index().on(table.email),
}));
export const posts = pgTable('posts', {
id: uuid('id').defaultRandom().primaryKey(),
title: varchar('title').notNull(),
content: text('content'),
published: boolean('published').default(false).notNull(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
authorId: uuid('authorId').notNull().references(() => users.id, { onDelete: 'cascade' }),
}, (table) => ({
authorIdIdx: index().on(table.authorId),
publishedCreatedAtIdx: index().on(table.published, table.createdAt),
}));
export const tags = pgTable('tags', {
id: uuid('id').defaultRandom().primaryKey(),
name: varchar('name').notNull().unique(),
});
export const subscriptions = pgTable('subscriptions', {
id: uuid('id').defaultRandom().primaryKey(),
tier: subscriptionTierEnum('tier').default('FREE').notNull(),
userId: uuid('userId').notNull().unique().references(() => users.id, { onDelete: 'cascade' }),
});
// Relations
export const usersRelations = relations(users, ({ one, many }) => ({
posts: many(posts),
subscription: one(subscriptions, {
fields: [users.id],
references: [subscriptions.userId],
}),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
tags: many(tags),
}));
export const tagsRelations = relations(tags, ({ many }) => ({
posts: many(posts),
}));
export const subscriptionsRelations = relations(subscriptions, ({ one }) => ({
user: one(users, {
fields: [subscriptions.userId],
references: [users.id],
}),
}));
Notice what SchemaForge preserved:
- Enums —
RoleandSubscriptionTierbecomepgEnum()definitions - Table mapping —
@@map("users")is reflected in thepgTable('users', ...)first argument - Relations — one-to-many and one-to-one relations use Drizzle's
relations()API - Indexes —
@@indexdirectives become Drizzle index definitions - Default values —
@default(uuid())→.defaultRandom(),@default(now())→.defaultNow() - Cascade deletes —
onDelete: Cascadeis preserved in the Drizzle.references()call - Unique constraints —
@uniqueon fields becomes.unique()in Drizzle
Step 4: Customize Type Mappings
Sometimes the default type mappings need adjustment. Maybe you want String in Prisma to map to text instead of varchar in Drizzle, or you need custom PostgreSQL types. SchemaForge supports custom type mappings via YAML or JSON config files:
# type-overrides.yaml
overrides:
prisma:
STRING: "String @db.Text"
drizzle:
STRING: "text"
Apply the overrides during conversion:
schemaforge convert --from prisma --to drizzle --input schema.prisma --type-map type-overrides.yaml
Template variables are available for parameterized types: {length}, {precision}, {scale}, {values}. This lets you write a single override that handles String @db.VarChar(255) and String @db.VarChar(1000) differently.
Step 5: Verify with Diff
After conversion, verify that nothing was lost. SchemaForge's diff command compares two schemas and shows line-level differences:
# Compare the original Prisma schema with a roundtrip back from Drizzle
schemaforge convert --from drizzle --to prisma --input schema.drizzle.ts --output schema-roundtrip.prisma
schemaforge diff schema.prisma schema-roundtrip.prisma
If the roundtrip produces the same schema, the conversion is lossless. If there are differences, the diff output shows exactly what changed:
Comparing schema.prisma vs schema-roundtrip.prisma
No differences found. Roundtrip is lossless.
Step 6: Generate Alembic Migrations
If your project uses Alembic for database migrations, SchemaForge can generate migration scripts from the converted schema:
# Generate an Alembic migration from the Drizzle schema
schemaforge convert --from drizzle --to alembic --input schema.drizzle.ts --output migrations/
# Or go directly from Prisma to Alembic
schemaforge convert --from prisma --to alembic --input schema.prisma --output migrations/
The generated migration includes upgrade() and downgrade() functions with the correct SQL operations for creating tables, indexes, enums, and foreign keys.
Roundtrip Verification
SchemaForge's shared IR guarantees zero-loss roundtripping. Here's the proof path:
# Original Prisma schema
schemaforge convert --from prisma --to sql --input schema.prisma --output schema-v1.sql
# Convert that SQL back to Prisma
schemaforge convert --from sql --to prisma --input schema-v1.sql --output schema-v2.prisma
# They should be identical
schemaforge diff schema.prisma schema-v2.prisma
The sql → prisma → sql roundtrip works the same way. This isn't an accident — it's a design guarantee of the IR architecture. Every format maps to the same intermediate representation, and every generator produces deterministic output from that representation.
Handling Edge Cases
Function Defaults
Prisma uses @default(now()), @default(uuid()), and @default(autoincrement()). SchemaForge preserves these using a fn: prefix convention:
@default(now())→ Drizzle's.defaultNow()@default(uuid())→ Drizzle's.defaultRandom()@default(autoincrement())→ Drizzle'sserial().primaryKey()- Other function defaults like
CURRENT_TIMESTAMPandgen_random_uuid()are preserved with thefn:prefix and roundtrip correctly
Custom Types
Prisma supports @db. annotations for database-specific types (@db.Uuid, @db.Jsonb, @db.Money). SchemaForge maps these to the appropriate Drizzle column types:
| Prisma Type | Drizzle Type |
|---|---|
String @db.Uuid |
uuid() |
String @db.Jsonb |
jsonb() |
String @db.Money |
numeric() |
String @db.VarChar(N) |
varchar({ length: N }) |
String @db.Text |
text() |
DateTime @db.Timestamptz |
timestamp({ withTimezone: true }) |
Types that don't have a direct mapping pass through as CUSTOM types, preserving the database-specific type name. You can then override these with custom type mappings.
Inline Enums
Prisma's enum blocks become Drizzle's pgEnum() for PostgreSQL. For MySQL, SchemaForge generates the appropriate mysqlEnum() call.
Composite Indexes
Prisma's multi-column indexes (@@index([published, createdAt])) map to Drizzle's composite index definitions. The column order is preserved.
MySQL-Specific Features
If your Prisma schema targets MySQL with ENGINE=InnoDB, AUTO_INCREMENT, or DEFAULT CHARSET, SchemaForge handles these in the SQL output path. The Drizzle generator targets the appropriate MySQL column types.
Get Started
Migrating from Prisma to Drizzle doesn't have to take days. With SchemaForge, the schema conversion takes seconds — and the roundtrip diff proves nothing was lost:
pip install git+https://github.com/Coding-Dev-Tools/schemaforge.git
schemaforge convert --from prisma --to drizzle --input schema.prisma
schemaforge diff schema.prisma schema-roundtrip.prisma # Verify
SchemaForge also converts to and from 9 other formats — SQL DDL, TypeORM, Django, SQLAlchemy, JSON Schema, GraphQL, EF Core (C#), and Scala case classes. See the SchemaForge vs Prisma Migrate vs Alembic vs Atlas comparison for a detailed breakdown.
For a live conversion experience in your editor, try the SchemaForge VS Code extension — it shows real-time schema previews and one-click conversion.
SchemaForge is part of the DevForge CLI tool suite — 11 developer tools for API contracts, schema conversion, infrastructure diffs, config drift, and more. All built by autonomous AI agents.
Get started
Install SchemaForge and convert your Prisma schema to Drizzle in seconds. No manual rewriting, no lost semantics.
View on GitHub →