| name | drizzle-orm |
| description | Drizzle ORM type-safe SQL queries, schema definition, migrations, and Supabase/Postgres integration. Triggers on: drizzle-orm, drizzle, pgTable, mysqlTable, eq(), and(), drizzle migrate, drizzle-kit, schema.ts drizzle.
|
Drizzle ORM
When to Use
Use when you need type-safe SQL with full TypeScript inference, want explicit control over queries (no magic), or are integrating with Supabase Postgres.
Core Rules
- Schema is the single source of truth — infer all types from it, never duplicate
- Always use
drizzle-kit for migrations — never write raw SQL migration files by hand
- Use
.returning() after insert/update to get the full row back
- Transactions for multi-table writes — always
- Prefer
.select({ col: table.col }) for partial selects over fetching full rows
Install
npm install drizzle-orm postgres
npm install -D drizzle-kit
Schema Definition
import {
pgTable, serial, text, varchar, integer, boolean,
timestamp, uuid, index, uniqueIndex, foreignKey,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable(
'users',
{
id: uuid('id').defaultRandom().primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: text('name').notNull(),
role: text('role', { enum: ['admin', 'user', 'viewer'] }).default('user').notNull(),
active: boolean('active').default(true).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
},
(table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
createdAtIdx: index('users_created_at_idx').on(table.createdAt),
})
);
export const posts = pgTable('posts', {
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
body: text('body'),
published: boolean('published').default(false).notNull(),
authorId: uuid('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
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;
Database Connection
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const connectionString = process.env.DATABASE_URL!;
export const migrationClient = postgres(connectionString, { max: 1 });
const queryClient = postgres(connectionString);
export const db = drizzle(queryClient, { schema });
Supabase Connection
const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString, { prepare: false });
export const db = drizzle(client, { schema });
Query API
Select
import { eq, and, or, like, gte, lte, desc, asc, isNull, inArray } from 'drizzle-orm';
const allUsers = await db.select().from(users);
const emails = await db.select({ id: users.id, email: users.email }).from(users);
const user = await db
.select()
.from(users)
.where(eq(users.email, 'user@example.com'))
.limit(1);
const activeAdmins = await db
.select()
.from(users)
.where(and(eq(users.active, true), eq(users.role, 'admin')));
const page = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.()
.();
results = db
.()
.(users)
.((users., ));
specific = db
.()
.(users)
.((users., [, , ]));
Insert
const [newUser] = await db
.insert(users)
.values({
email: 'user@example.com',
name: 'Example User',
role: 'admin',
})
.returning();
const newPosts = await db
.insert(posts)
.values([
{ title: 'First Post', authorId: newUser.id },
{ title: 'Second Post', authorId: newUser.id },
])
.returning();
await db
.insert(users)
.values({ email: 'user@example.com', name: 'Example User' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'Example User Updated', updatedAt: new Date() },
});
Update
const [updated] = await db
.update(users)
.set({ name: 'Example User F', updatedAt: new Date() })
.where(eq(users.id, userId))
.returning();
await db
.update(posts)
.set({ published: true })
.where(and(eq(posts.authorId, userId), eq(posts.published, false)));
Delete
const [deleted] = await db
.delete(users)
.where(eq(users.id, userId))
.returning();
await db
.update(users)
.set({ active: false, updatedAt: new Date() })
.where(eq(users.id, userId));
Joins
import { sql } from 'drizzle-orm';
const postsWithAuthors = await db
.select({
postId: posts.id,
title: posts.title,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt));
const usersWithPosts = await db
.select({
user: users,
postCount: sql<number>`count(${posts.id})::int`,
})
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId))
.groupBy(users.id);
Relational Query API (with relations defined)
const usersWithPosts = await db.query.users.findMany({
where: eq(users.active, true),
with: {
posts: {
where: eq(posts.published, true),
orderBy: [desc(posts.createdAt)],
limit: 5,
},
},
limit: 10,
});
Transactions
const result = await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ email: 'new@example.com', name: 'New User' })
.returning();
const [post] = await tx
.insert(posts)
.values({ title: 'First Post', authorId: user.id })
.returning();
if (!post) throw new Error('Post creation failed');
return { user, post };
});
Migrations (drizzle-kit)
Config
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Commands
npx drizzle-kit generate
npx drizzle-kit push
npx drizzle-kit migrate
npx drizzle-kit studio
npx drizzle-kit check
Run Migrations Programmatically
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const client = postgres(process.env.DATABASE_URL!, { max: 1 });
const db = drizzle(client);
await migrate(db, { migrationsFolder: './drizzle' });
await client.end();
Raw SQL When Needed
import { sql } from 'drizzle-orm';
const result = await db
.select({ count: sql<number>`count(*)::int` })
.from(users);
const rows = await db.execute(
sql`SELECT * FROM users WHERE email ILIKE ${'%' + search + '%'} LIMIT 10`
);
Type Inference Patterns
type User = typeof users.$inferSelect;
type NewUser = typeof users.$inferInsert;
type UserPreview = Pick<User, 'id' | 'name' | 'email'>;
type UserWithPosts = typeof users.$inferSelect & {
posts: typeof posts.$inferSelect[];
};
Quick Reference
| Task | Pattern |
|---|
| Insert + get row | .insert().values().returning() |
| Upsert | .onConflictDoUpdate({ target, set }) |
| Pagination | .limit(n).offset(n) |
| Joins | .innerJoin(table, eq(a.id, b.fk)) |
| Nested data | db.query.table.findMany({ with: {} }) |
| Raw SQL | sql\...`` template tag |
| Multi-table write | db.transaction(async (tx) => { ... }) |
| Supabase pooler | postgres(url, { prepare: false }) |
Related Skills
prisma-patterns — alternative ORM
database-schema-designer — schema design
database-migration-strategies — migrations
GitNexus Index
This skill is indexed by GitNexus for knowledge graph traversal.
Index path: /Users/localuser/.claude/skills/drizzle-orm/.gitnexus
Last indexed: 2026-05-23