| name | drizzle |
| description | Drizzle ORM patterns for PostgreSQL schemas, queries, migrations, and Zod integration. Triggers on drizzle, pgTable, db.select, db.insert, migration. |
| triggers | drizzle, pgTable, db\.select, db\.insert, db\.update, db\.delete, db\.query, migration, drizzle-zod |
MCPSearch({ query: "select:mcp__plugin_devtools_context7__query-docs" })
// Drizzle schema patterns
mcp__context7__query_docs({
libraryId: "/drizzle-team/drizzle-orm",
query: "How do I define tables with pgTable, text, boolean, and timestamp?",
});
// Query patterns
mcp__context7__query_docs({
libraryId: "/drizzle-team/drizzle-orm",
query: "How do I use db.select, db.insert, db.update, and db.delete?",
});
// Relations
mcp__context7__query_docs({
libraryId: "/drizzle-team/drizzle-orm",
query: "How do I define relations with one and many, and use db.query?",
});
// Migrations
mcp__context7__query_docs({
libraryId: "/drizzle-team/drizzle-orm",
query: "How do I use drizzle-kit for generate, migrate, and push?",
});
Note: Context7 v2 uses server-side filtering. Use descriptive natural language queries.
| Template | Purpose |
|---|---|
templates/table-schema.ts.md |
Table definition with types |
templates/queries.ts.md |
CRUD query patterns |
Define a table:
import { pgTable, text, boolean, timestamp, index } from "drizzle-orm/pg-core";
export const contacts = pgTable(
"contact",
{
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
enabled: boolean("enabled").default(true),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => [index("idx_contact_user_id").on(table.userId)],
);
// Always export inferred types
export type InsertContact = typeof contacts.$inferInsert;
export type SelectContact = typeof contacts.$inferSelect;
Query patterns:
import { eq, and, desc } from "drizzle-orm";
// Select with filter
const results = await db
.select()
.from(contacts)
.where(eq(contacts.userId, userId))
.orderBy(desc(contacts.createdAt));
// Insert
await db
.insert(contacts)
.values({ id: crypto.randomUUID(), name, email, userId });
// Update
await db.update(contacts).set({ enabled: false }).where(eq(contacts.id, id));
// Delete
await db.delete(contacts).where(eq(contacts.id, id));
Required:
- Every table exports
$inferInsertand$inferSelecttypes - All timestamps use
{ withTimezone: true } - Foreign keys have
onDeleteaction specified - Indexes defined for foreign keys and frequently queried columns
- Import directly from source files, not index barrels
- Gate test database clients with explicit
NODE_ENV === "production"check - Prefer simplest schema design
- Delete unused columns/tables completely
Naming: Schema files=lowercase-with-hyphens.ts, Tables=singular noun
When multiple branches add migrations with overlapping indices, resolve by renumbering:
- Keep HEAD's migrations in place (they may already be applied)
- Renumber incoming migrations to fill subsequent indices
- Critical: Update each snapshot's
prevIdto form a proper chain
// Each snapshot's prevId must reference the PREVIOUS snapshot's id
// 0020_snapshot.json: { "id": "abc123", "prevId": "xyz789" }
// 0021_snapshot.json: { "id": "def456", "prevId": "abc123" } ← Must chain!
Drizzle migrations form a linked list via prevId. If two snapshots share the same prevId, migrations will fail.
Custom Type Normalization
Use custom types for automatic data normalization at the database boundary:
// Define custom type that normalizes on write
export const evmAddress = customType<{ data: string }>({
dataType() {
return "text";
},
toDriver(value) {
return value.toLowerCase();
}, // Auto-normalize
});
// Use in schema - no manual toLowerCase() needed
export const contracts = pgTable("contracts", {
address: evmAddress("address").notNull(),
});
This eliminates manual normalization throughout the codebase and ensures consistency.
- Context7 docs fetched for current API
- Table has
$inferInsertand$inferSelecttype exports - All timestamps use
{ withTimezone: true } - Foreign keys have
onDeleteaction - Indexes defined for foreign keys
- Migration generated and tested