Skip to main content
SQL & Database · 100% Client-Side Engine

SQL to Drizzle ORM Schema Converter

Convert PostgreSQL, MySQL, and SQLite CREATE TABLE DDL into Drizzle ORM TypeScript schemas (pgTable, mysqlTable, sqliteTable, relations, and $inferSelect).

SQL to Drizzle Schema100% Client-Side Engine
Output Configuration Options
Target Drizzle ORM core package and column builders
Map snake_case SQL columns to camelCase TypeScript properties
Emit Drizzle Relational Queries API definitions (one / many)
Export TypeScript select and insert row types for each table
Input (sql)18 lines
Output (typescript)52 lines

Example Output (SQL to Drizzle Schema)

SAMPLE INPUT (SQL)
CREATE TYPE user_role AS ENUM ('ADMIN', 'MEMBER', 'VIEWER');

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) NOT NULL UNIQUE,
  role user_role NOT NULL DEFAULT 'MEMBER',
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title VARCHAR(180) NOT NULL,
  metadata JSONB,
  published BOOLEAN NOT NULL DEFAULT false
);

CREATE INDEX idx_posts_author ON posts (author_id, published);
GENERATED OUTPUT (TYPESCRIPT)
import {
  boolean,
  index,
  jsonb,
  pgEnum,
  pgTable,
  serial,
  timestamp,
  uuid,
  varchar,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

export const userRoleEnum = pgEnum("user_role", ["ADMIN", "MEMBER", "VIEWER"]);

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

export const posts = pgTable(
  "posts",
  {
    id: serial("id").primaryKey(),
    authorId: uuid("author_id").notNull().references(() => users.id, { onDelete: "cascade" }),
    title: varchar("title", { length: 180 }).notNull(),
    metadata: jsonb("metadata"),
    published: boolean("published").default(false).notNull(),
  },
  (t) => ({
    idxPostsAuthor: index("idx_posts_author").on(t.authorId, t.published),
  })
);

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

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

Understanding the SQL to Drizzle Schema Transformation

Migrating raw SQL DDL statements (`CREATE TABLE`, `CREATE TYPE ... AS ENUM`, and `CREATE INDEX`) into Drizzle ORM requires mapping SQL types into dialect-specific column builders from `drizzle-orm/pg-core`, `drizzle-orm/mysql-core`, or `drizzle-orm/sqlite-core`. Unlike Prisma's custom DSL, Drizzle defines schemas directly in TypeScript where foreign keys require both database-level `.references(() => table.col)` constraints (for `drizzle-kit` migrations) and application-level `relations()` declarations (for `db.query.*` relational queries). This converter parses multi-table SQL schemas entirely in your browser and outputs idiomatic Drizzle TypeScript code.

Key Architectural Benefits

  • •Generates both database-level `.references(() => parent.id, { onDelete })` constraints and Drizzle Query API `relations(table, ({ one, many }) => ...)` blocks.
  • •Supports PostgreSQL (`pgTable`, `pgEnum`, `uuid().defaultRandom()`), MySQL (`mysqlTable`, `varchar`), and SQLite/Turso (`sqliteTable`, `integer({ mode: 'boolean' })`).
  • •Translates composite `PRIMARY KEY (a, b)` constraints and `CREATE INDEX` statements into Drizzle's third-argument table callback `(t) => ({ pk: primaryKey(...), idx: index(...) })`.
  • •Exports modern `typeof table.$inferSelect` and `typeof table.$inferInsert` TypeScript types instead of deprecated `InferModel` helpers.

Common Conversion Pitfalls & Fixes

⚠️ Pitfall: Defining .references() without relations() and wondering why db.query.*.findMany({ with: ... }) fails
Solution: In Drizzle ORM, `.references(() => users.id)` only defines the SQL `FOREIGN KEY` constraint for migrations. To use Drizzle's relational query builder (`db.query.users.findMany({ with: { posts: true } })`), you must also export `relations(users, ({ many }) => ({ posts: many(posts) }))`.
⚠️ Pitfall: Using JavaScript number mode on 64-bit BIGINT columns that exceed Number.MAX_SAFE_INTEGER
Solution: Drizzle's `bigint('col', { mode: 'number' })` is convenient for standard auto-increment IDs under `2^53 - 1`, but if you store 64-bit Snowflake IDs, switch to `{ mode: 'bigint' }` so values are not silently rounded in JavaScript.
⚠️ Pitfall: Forgetting that SQLite lacks native BOOLEAN and TIMESTAMP column types
Solution: When targeting `drizzle-orm/sqlite-core`, use `integer('col', { mode: 'boolean' })` and `integer('col', { mode: 'timestamp' })` so Drizzle automatically serializes booleans (`0`/`1`) and JavaScript `Date` objects.

Verified Edge Cases & Complex Scenarios

Real payloads often contain edge cases that break naive generators. Here is how our engine handles them:

1. Many-to-Many Join Tables with Composite Primary Keys

A join table defines `PRIMARY KEY (post_id, tag_id)` across two foreign keys instead of a single `id` column.

INPUT (SQL)
CREATE TABLE post_tags (
  post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
  PRIMARY KEY (post_id, tag_id)
);
OUTPUT (TYPESCRIPT)
export const postTags = pgTable(
  "post_tags",
  {
    postId: integer("post_id").notNull().references(() => posts.id, { onDelete: "cascade" }),
    tagId: integer("tag_id").notNull().references(() => tags.id, { onDelete: "cascade" }),
  },
  (t) => ({
    pk: primaryKey({ columns: [t.postId, t.tagId] }),
  })
);
Why: Single-column primary keys use `.primaryKey()` directly on the column builder, whereas composite primary keys must be returned inside the table configuration callback using `primaryKey({ columns: [...] })`.
2. PostgreSQL Native ENUMs and TIMESTAMPTZ Defaults

A PostgreSQL schema declares `CREATE TYPE status AS ENUM` alongside `TIMESTAMPTZ DEFAULT NOW()` and `UUID DEFAULT gen_random_uuid()`.

INPUT (SQL)
CREATE TYPE order_status AS ENUM ('PENDING', 'PAID');
CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  status order_status NOT NULL DEFAULT 'PENDING',
  placed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
OUTPUT (TYPESCRIPT)
export const orderStatusEnum = pgEnum("order_status", ["PENDING", "PAID"]);

export const orders = pgTable("orders", {
  id: uuid("id").primaryKey().defaultRandom(),
  status: orderStatusEnum("status").default("PENDING").notNull(),
  placedAt: timestamp("placed_at", { withTimezone: true }).defaultNow().notNull(),
});
Why: PostgreSQL enums must be declared as standalone exported constants via `pgEnum()` so `drizzle-kit` generates the `CREATE TYPE` migration before the table.

Frequently Asked Questions

How do I infer TypeScript row types from a Drizzle table?
Modern Drizzle ORM (v0.28+) provides built-in `$inferSelect` and `$inferInsert` properties directly on every table object: `export type User = typeof users.$inferSelect` and `export type NewUser = typeof users.$inferInsert`.
What is the difference between .references() and relations() in Drizzle ORM?
`.references(() => users.id)` generates the physical SQL `FOREIGN KEY` constraint in the database via `drizzle-kit`. `relations()` is an application-level declaration that tells Drizzle's TypeScript query builder how to join tables when using `db.query`.
Is my SQL schema sent to any server during conversion?
No. The SQL DDL lexer and Drizzle code generator run 100% locally inside your browser tab with zero network requests.
Browse All