Transform hub-guide from a single-skill Hub monorepo guide into a comprehensive programming best-practice plugin covering all situations. Skills (26): - Core: engineering-principles, clean-code, clean-architecture, design-patterns, testing, error-handling, security, api-design, git-workflow, documentation, logging-observability, performance - Languages: typescript, python, rust, go - Frameworks: react-frontend, elysiajs, hono-backend, drizzle-database, nextjs - Infrastructure: docker, ci-cd, monitoring - Monorepo: monorepo, hub-guide (existing) Hooks: - SessionStart: auto-detect project type and activate relevant skills - PreToolUse (Write|Edit): inject language-specific rules per file type Reference files for deep dives: - clean-architecture/references/solid.md (SOLID + component principles) - design-patterns/references/catalog.md (full GoF catalog with examples) - testing/references/mocks.md (test double taxonomy) Restructure plugin to modern skills/ directory format.
6.3 KiB
6.3 KiB
name, description
| name | description |
|---|---|
| drizzle-database | Drizzle ORM best practices — schema design, queries, migrations, relations, and performance. Use when designing database schemas, writing Drizzle queries, managing migrations, or whenever the user mentions "Drizzle," "Drizzle ORM," "drizzle-orm," "drizzle-kit," "schema," "migration," "PostgreSQL," "SQLite," or "database design." |
Drizzle ORM Best Practices
Schema Design
import { pgTable, serial, text, timestamp, boolean, integer, jsonb } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Tables with explicit foreign keys
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
role: text('role', { enum: ['admin', 'user'] }).default('user').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const orders = pgTable('orders', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
status: text('status', { enum: ['pending', 'paid', 'shipped'] }).default('pending').notNull(),
total: integer('total').notNull(), // in cents
metadata: jsonb('metadata'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Relations
export const usersRelations = relations(users, ({ many }) => ({
orders: many(orders),
}));
export const ordersRelations = relations(orders, ({ one }) => ({
user: one(users, { fields: [orders.userId], references: [users.id] }),
}));
Rules:
serialfor auto-increment PKs,uuidfor distributed/public IDs.timestampwithdefaultNow()for created/updated.- JSONB for flexible metadata (Postgres). Text JSON for SQLite.
- Enums as
textwithenumconstraint (not PostgresCREATE TYPE— easier migrations).
Queries
Basic CRUD
import { eq, and, or, like, gte, lte, asc, desc, sql, inArray } from 'drizzle-orm';
// Create
const [user] = await db.insert(users).values({ email: 'a@b.com' }).returning();
// Read
const allUsers = await db.select().from(users);
const user = await db.select().from(users).where(eq(users.id, id)).limit(1);
const admins = await db.select().from(users).where(eq(users.role, 'admin')).orderBy(desc(users.createdAt));
// Update
const [updated] = await db.update(users).set({ name }).where(eq(users.id, id)).returning();
// Delete
await db.delete(users).where(eq(users.id, id));
Joins
// One-to-many
const result = await db.select()
.from(users)
.leftJoin(orders, eq(users.id, orders.userId))
.where(eq(users.id, id));
// With relations (prepared — uses multiple queries or JOINs internally)
const userWithOrders = await db.query.users.findFirst({
where: eq(users.id, id),
with: { orders: { limit: 5 } },
});
Aggregations
import { count, sum, avg, min, max, sql } from 'drizzle-orm';
const stats = await db.select({
total: count(),
totalRevenue: sum(orders.total),
avgOrderValue: avg(orders.total),
byStatus: sql`${orders.status}::text`,
}).from(orders)
.groupBy(orders.status);
Migrations (drizzle-kit)
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema/*.ts',
out: './src/db/migrations',
dbCredentials: { url: process.env.DATABASE_URL! },
});
# Commands
bunx drizzle-kit generate # Generate migration from schema changes
bunx drizzle-kit migrate # Apply migrations to database
bunx drizzle-kit push # Push schema (dev only — no migration files)
bunx drizzle-kit studio # Drizzle Studio (GUI for DB inspection)
Rules:
- Generate migrations, then apply. Use
drizzle-kit pushonly in dev. - Code review migration files before applying to production.
- Write custom SQL for complex migrations (backfills, data transformations).
- Never edit generated migration files manually (unless you know what you're doing).
Performance
Indexes
import { index, uniqueIndex } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
// ...
}, (table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
roleIdx: index('users_role_idx').on(table.role),
// Composite index for common queries
createdRoleIdx: index('users_created_role_idx').on(table.createdAt, table.role),
}));
N+1 Prevention
// ❌ N+1 — one query per order item
for (const order of orders) {
const items = await db.select().from(orderItems).where(eq(orderItems.orderId, order.id));
}
// ✅ Eager with `IN`
const orderIds = orders.map(o => o.id);
const allItems = await db.select().from(orderItems).where(inArray(orderItems.orderId, orderIds));
Prepared Statements
const findUserByEmail = db.select().from(users).where(eq(users.email, sql.placeholder('email'))).prepare();
// Reuse
const user1 = await findUserByEmail.execute({ email: 'a@b.com' });
const user2 = await findUserByEmail.execute({ email: 'c@d.com' });
Repository Pattern
// Always accessed through repository — never direct db calls from routes
export class UserRepository {
constructor(private db: DB) {}
async findByEmail(email: string): Promise<User | null> {
const result = await this.db.select().from(users).where(eq(users.email, email)).limit(1);
return result[0] ?? null;
}
async create(input: CreateUserInput): Promise<User> {
const [user] = await this.db.insert(users).values(input).returning();
return user;
}
async update(id: number, data: Partial<User>): Promise<User | null> {
const [user] = await this.db.update(users).set({ ...data, updatedAt: new Date() }).where(eq(users.id, id)).returning();
return user ?? null;
}
}
Anti-patterns
- ❌ Raw SQL strings instead of Drizzle query builder when Drizzle provides it
- ❌
select *in production — name specific columns - ❌ No repository layer — Drizzle queries in route handlers
- ❌ Missing indexes on foreign keys and filtered columns
- ❌
awaitin loops for sequential queries — usePromise.allorINqueries - ❌ Editing generated migration files
- ❌ Using
serialfor user-facing IDs — useuuidfor public IDs