Convert SQL Schema to Prisma and Drizzle Without Retyping a Single Column (2026)

How to turn a raw pg_dump or MySQL DDL file into a working schema.prisma or Drizzle schema.ts, complete with relations, enums, and indexes intact.

SB

SmartBuddy Engineering Team

Autonomous Systems & Database & ORM Engineering
Convert SQL Schema to Prisma and Drizzle Without Retyping a Single Column (2026)

⚡ Key Takeaways

  • The manual path breaks on the first foreign key. Hand-porting a 12-table PostgreSQL dump into Prisma model blocks means re-deriving every @relation, every @map, and every cascade rule by eye — miss one and the migration silently drops referential integrity.
  • Naming has to survive the trip. created_at in the database still needs to read as createdAt in application code, in both Prisma (@map) and Drizzle (the column-name string argument) — not one or the other.
  • Enums and UUIDs are where converters usually guess wrong. A PostgreSQL ENUM type and a gen_random_uuid() default each map to a specific, non-obvious construct in both ORMs, and getting either wrong fails prisma validate or TypeScript strict mode immediately.
  • This is a 6-phase workflow, not a one-shot regex. Parsing DDL, mapping the foreign-key graph, generating Prisma, generating Drizzle, validating both outputs, then producing a conversion report are separate steps — skipping validation is how broken schemas end up in a PR.

Why Converting SQL to Prisma or Drizzle by Hand Goes Wrong

A pg_dump --schema-only on a real production database rarely produces 3 tables. It produces 15, 40, sometimes more, with composite indexes, ON DELETE CASCADE chains, and custom ENUM types scattered across the file. Rewriting that as Prisma model blocks or Drizzle pgTable calls by hand means holding the entire foreign-key graph in your head while you type, one missed @relation(fields: [...], references: [...]) and a 1:N join silently becomes an orphaned column with no relation at all.

The SQL DDL to Prisma & Drizzle ORM Converter is an autonomous agent skill built to remove that translation step. It ingests raw CREATE TABLE, CREATE INDEX, CREATE TYPE, and ALTER TABLE ADD CONSTRAINT statements from PostgreSQL or MySQL 8+, and outputs a formatted schema.prisma or a strongly-typed Drizzle schema.ts, with the relation graph, enums, and index definitions already wired.

How the Conversion Actually Works

The skill runs a fixed 6-phase sequence rather than a single generation pass:

  1. DDL parsing. Reads table statements and maps column types (VARCHAR, UUID, TIMESTAMPTZ, JSONB, BOOLEAN), primary keys, and nullability.
  2. Foreign-key graph mapping. Resolves every FOREIGN KEY constraint into a 1:N, M:N (via join table), or 1:1 relationship before any ORM code is written.
  3. Prisma synthesis. Generates a schema.prisma targeting whatever generator the project's Prisma version actually uses (current Prisma moved to a prisma-client generator with an explicit output path, an existing project still on the older prisma-client-js keeps its own config rather than getting silently upgraded mid-conversion), with @map, @relation, @@index, and @@map directives.
  4. Drizzle synthesis. Generates a matching TypeScript schema.ts using pgTable and pgEnum for the tables, plus whichever relational-query API the project is on for the relations themselves, the older relations() helper or the newer Relational Queries v2 defineRelations(), which aren't interchangeable.
  5. Validation. Checks the output against whichever target was actually generated, prisma validate only applies if Prisma was a target; a Drizzle-only conversion validates via TypeScript typechecking instead.
  6. Conversion report. Produces a report of what actually made it across, tables, columns, foreign keys, indexes, and enums converted vs. total, instead of ending on a bare "Done!"

That validation phase is the one manual conversions almost always skip, because there's no equivalent of "compile the schema" step when you're typing it by hand, you find out it's broken only when a query fails at runtime.

Prisma and Drizzle Output, Side by Side

Both ORMs solve the same problem, type-safe database access from TypeScript, but they encode a foreign key differently. Here's the same orders.user_id → users.id relationship as the skill emits it in each:

Concept Prisma (schema.prisma) Drizzle (schema.ts)
Snake_case → camelCase userId String @map("user_id") userId: uuid("user_id")
Foreign key + cascade user User @relation(fields: [userId], references: [id], onDelete: Cascade) .references(() => users.id, { onDelete: "cascade" })
Reverse relation orders Order[] on the User model relations(users, ({ many }) => ({ orders: many(orders) }))
Enum enum SubscriptionTier { FREE PRO ENTERPRISE } pgEnum("subscription_tier", ["FREE","PRO","ENTERPRISE"])
UUID default @default(dbgenerated("gen_random_uuid()")) @db.Uuid uuid("id").primaryKey().defaultRandom()
Composite index @@index([userId]) defined alongside the table via Drizzle's index builder

A Real Conversion: One Foreign Key, Both Targets

Take a minimal two-table PostgreSQL DDL, a users table with a subscription tier enum, and an orders table referencing it:

CREATE TYPE subscription_tier AS ENUM ('FREE', 'PRO', 'ENTERPRISE');

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  tier subscription_tier NOT NULL DEFAULT 'FREE',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  total_amount DECIMAL(10,2) NOT NULL
);

The skill's Prisma output preserves the cascade rule and adds the reverse relation on User automatically:

model User {
  id        String           @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  email     String           @unique @db.VarChar(255)
  tier      SubscriptionTier @default(FREE)
  createdAt DateTime         @default(now()) @map("created_at") @db.Timestamptz(6)
  orders    Order[]

  @@map("users")
}

model Order {
  id          String  @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  userId      String  @map("user_id") @db.Uuid
  totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
  user        User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@map("orders")
}

And the Drizzle equivalent, generated from the same DDL in the same pass:

import { pgTable, uuid, varchar, timestamp, decimal, pgEnum } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

export const subscriptionTierEnum = pgEnum("subscription_tier", ["FREE", "PRO", "ENTERPRISE"]);

export const users = pgTable("users", {
  id: uuid("id").primaryKey().defaultRandom(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  tier: subscriptionTierEnum("tier").default("FREE").notNull(),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});

export const orders = pgTable("orders", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
  totalAmount: decimal("total_amount", { precision: 10, scale: 2 }).notNull(),
});

export const usersRelations = relations(users, ({ many }) => ({
  orders: many(orders),
}));

export const ordersRelations = relations(orders, ({ one }) => ({
  user: one(users, { fields: [orders.userId], references: [users.id] }),
}));

That relations() helper is the older (v1) way to declare Drizzle relations, for a project already on the newer Relational Queries v2 API, the same relationship comes out as a single defineRelations() call using from/to/alias instead of fields/references/relationName. The skill detects which one the project is actually using rather than assuming.

Scale that same pattern to a real dump, 15 tables, a dozen enums, mixed 1:N and M:N relations through join tables, and the value isn't the syntax translation itself. It's that both files come out consistent with each other, generated from the same foreign-key graph in the same run.

Running It

The skill takes a single natural-language prompt plus the DDL file itself:

"Using the sql-to-prisma-drizzle-converter skill, convert this 10-table
PostgreSQL DDL file into a modern, formatted schema.prisma with relations
and indexes."

Or, for a MySQL source targeting Drizzle instead:

"Using the sql-to-prisma-drizzle-converter skill, translate this MySQL
e-commerce SQL schema into type-safe Drizzle ORM TypeScript models."

It runs inside Claude Code, Cursor, Windsurf, Gemini CLI, Antigravity, or OpenHands, no external network calls, since the DDL and the generated schema files never leave the local workspace.

Where This Skill Stops

It generates the schema files. It does not run a live migration against your database, and it does not replace prisma migrate or drizzle-kit push, those still apply the schema once you've reviewed it. Treat the output as a first draft that passes validation, not a final, unreviewed commit.

Frequently Asked Questions

Which SQL dialects and target ORMs does it support?

PostgreSQL and MySQL 8+ as input dialects, the two aren't just a datasource-string swap; MySQL gets its own type mapping (AUTO_INCREMENT, ENUM(...) syntax, mysqlTable/mysql-core) rather than reused Postgres-flavored output. On the output side, Prisma (schema.prisma, generator version matched to the project) and Drizzle ORM (relation API version matched to the project), targeting either drizzle-orm/pg-core or drizzle-orm/mysql-core depending on the source dialect.

Does it handle composite indexes and many-to-many relations?

Yes, including composite primary keys, composite foreign keys, and composite unique constraints, which need their own Prisma (@@id([...])) and Drizzle (primaryKey({ columns: [...] })) syntax rather than an extension of the single-column case. FOREIGN KEY ... REFERENCES constraints become Prisma @relation directives and Drizzle relations, CREATE INDEX statements become @@index / Drizzle's index(), and join tables are recognized for M:N relationships rather than emitted as plain two-column tables.

What happens to enums and UUID defaults during conversion?

PostgreSQL ENUM types map to a Prisma enum block and a Drizzle pgEnum() call using the same value list. gen_random_uuid() and cuid() defaults, timestamp defaults, and JSONB columns are translated to their exact typed equivalents in both ORMs, not approximated.

What if my input is a full pg_dump, not just table definitions?

A real pg_dump often includes views, materialized views, sequences, triggers, functions, and RLS policies alongside the tables. Coverage differs by target, Drizzle can represent views, materialized views, sequences, and RLS policies directly; Prisma has no schema-level equivalent for any of those, and neither ORM has one for triggers or functions. Rather than silently dropping anything outside CREATE TABLE/CREATE INDEX/CREATE TYPE, the conversion ends with a report listing exactly what converted, what needs a manual look, and what isn't represented at all, not just a bare "done."

Did you find this technical breakdown helpful?

Tap to rate this guide · 8 views

Comments

Comments are reviewed before appearing publicly.

No comments yet — be the first.

🚀 Ready to Deploy Autonomous Skills in Production?

Get this skill (and 29 more) in the SmartBuddy Shop, or work with our engineering team to architect custom multi-agent workflows for your company.