db
// This skill provides database patterns for PostgreSQL with Drizzle ORM. Run `/db` for full reference.
$ git log --oneline --stat
stars:4
forks:1
updated:February 6, 2026
SKILL.mdreadonly
Database Skill (Drizzle ORM)
This skill provides database patterns for PostgreSQL with Drizzle ORM. Run /db for full reference.
Project Structure
src/db/
├── index.ts # Singleton db connection
├── schema.ts # All table definitions
├── migrate.ts # Migration runner
└── repositories/ # Data access layer (REQUIRED)
CRITICAL: Always Use Repositories
NEVER access db directly. Always use repository classes.
// ✅ CORRECT
import { featureRepository } from '@/db/repositories/feature.repository';
const feature = await featureRepository.findById(id);
// ❌ WRONG - Direct db access
import { db } from '@/db';
const feature = await db.select().from(features); // NEVER DO THIS
Schema Definition
// src/db/schema.ts
import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
content: text('content'),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
status: text('status').notNull().default('draft'),
priority: integer('priority').notNull().default(0),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Type exports
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
Schema Conventions
- Table names: lowercase plural (
users,posts) - Column names: snake_case (
created_at,user_id) - Primary keys: UUID with
defaultRandom() - Timestamps: Always include
created_atandupdated_at
Repository Pattern
// src/db/repositories/post.repository.ts
import { eq, and, desc, asc } from 'drizzle-orm';
import { db, schema } from '@/db';
import type { Post, NewPost } from '@/db/schema';
export class PostRepository {
async findById(id: string): Promise<Post | null> {
return await db.query.posts.findFirst({
where: eq(schema.posts.id, id),
}) ?? null;
}
async findMany(limit = 10, offset = 0): Promise<Post[]> {
return await db.query.posts.findMany({
limit,
offset,
orderBy: desc(schema.posts.createdAt),
});
}
async findByUserId(userId: string): Promise<Post[]> {
return await db.query.posts.findMany({
where: eq(schema.posts.userId, userId),
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
});
}
async create(data: NewPost): Promise<Post> {
const result = await db
.insert(schema.posts)
.values({
...data,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning();
return result[0];
}
async update(id: string, data: Partial<NewPost>): Promise<Post | null> {
const result = await db
.update(schema.posts)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(schema.posts.id, id))
.returning();
return result[0] ?? null;
}
async delete(id: string): Promise<boolean> {
const result = await db
.delete(schema.posts)
.where(eq(schema.posts.id, id))
.returning();
return result.length > 0;
}
}
export const postRepository = new PostRepository();
Migration Workflow
# 1. Edit src/db/schema.ts
# 2. Generate migration
bun db:generate --name=create_posts_table
# 3. Review SQL in drizzle/migrations/
# 4. Apply migration
bun db:migrate
# 5. Commit migration files
git add drizzle/migrations/
Migration Naming
- Use snake_case:
create_posts_table,add_user_role_column - Start with action:
create_,add_,update_,remove_
Common Query Patterns
Filtering with Multiple Conditions
import { eq, and, or, isNull, isNotNull, gt, lt, like } from 'drizzle-orm';
// AND conditions
where: and(
eq(schema.posts.userId, userId),
eq(schema.posts.status, 'published')
)
// OR conditions
where: or(
eq(schema.posts.status, 'draft'),
eq(schema.posts.status, 'review')
)
// NULL checks
where: isNull(schema.posts.deletedAt)
// Comparisons
where: gt(schema.posts.priority, 5)
// LIKE
where: like(schema.posts.title, '%search%')
Ordering
// Single column
orderBy: desc(schema.posts.createdAt)
// Multiple columns
orderBy: (posts, { asc, desc }) => [
asc(posts.status),
desc(posts.priority),
desc(posts.createdAt),
]
Relations (with query API)
// In schema.ts - define relations
import { relations } from 'drizzle-orm';
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.userId],
references: [users.id],
}),
comments: many(comments),
}));
// In repository - query with relations
async findWithAuthor(id: string) {
return await db.query.posts.findFirst({
where: eq(schema.posts.id, id),
with: {
author: true,
comments: true,
},
});
}
Database Client Setup
// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!, {
max: process.env.NODE_ENV === 'production' ? 10 : 1,
idle_timeout: 20,
max_lifetime: 60 * 30,
});
export const db = drizzle(client, { schema });
export type Database = typeof db;
export { schema };
Common Imports
// Schema and types
import { db, schema } from '@/db';
import type { Post, NewPost } from '@/db/schema';
// Query operators
import { eq, and, or, gt, lt, gte, lte, like, isNull, isNotNull, desc, asc } from 'drizzle-orm';
// Table definitions (for insert/update)
import { pgTable, uuid, text, timestamp, integer, jsonb, boolean } from 'drizzle-orm/pg-core';
// Relations
import { relations } from 'drizzle-orm';