skills/drizzle-orm/SKILL.md
Drizzle ORM v1 beta query patterns for TypeScript. Use when writing, reviewing, or debugging Drizzle queries on `[email protected]+`, especially with Relations v2, Relational Query Builder v2 (`db.query.*`), dynamic filters, SQL builder fallbacks, relation loading, and query-shape type mismatches. Also apply during code review when code contains non-trivial `drizzle-orm` usage.
npx skillsauth add perdolique/workflow drizzle-ormInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Assume Drizzle 1.0.0-beta.1+ and Relational Query Builder v2 unless the
installed package or codebase explicitly proves otherwise. In this model,
db.query.* is the v2 API and relations are defined with defineRelations.
For formatting of relational queries and SQL builder chains, especially when a
query mixes columns, where, and with or builder helpers like .select()
and .where(), read references/formatting.md. Use
that layout consistently in generated examples and code review suggestions
because deep Drizzle configs get hard to scan when everything is packed onto a
few lines.
Treat these as current Drizzle v1 beta patterns:
defineRelations(schema, (relationBuilder) => ...) from drizzle-orm.drizzle(client, { relations }).db.query.table.findMany() and db.query.table.findFirst() for RQB v2.where and orderBy in relational queries.Do not add compatibility branches unless the current project has a reproduced need for them. Prefer keeping new code in the v2 shape.
Drizzle query advice depends not only on query shape, but also on the database driver in use. Before suggesting transactions or multi-step write fixes, check which client the code is actually using.
If the code uses drizzle-orm/neon-http, do not suggest a normal
db.transaction(async (tx) => ...) fix. The Neon HTTP driver does not support
Drizzle transaction callbacks, so advice that assumes transactional writes is
incorrect for that client.
If the code uses drizzle-orm/neon-serverless with a WebSocket or Pool
client, transactions are supported and db.transaction(...) is a valid option.
When a handler performs multiple writes that must succeed or fail together, first identify the client:
db.transaction(...) on that same client without verifying a
supported transactional path.This is especially important during code review: avoid suggesting fixes that the current driver cannot execute.
Define relations in one place with defineRelations. The relationBuilder
callback parameter exposes schema tables plus relation helpers such as one,
many, and through.
import { defineRelations } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/node-postgres'
import * as schema from './schema'
export const relations = defineRelations(schema, (relationBuilder) => ({
posts: {
author: relationBuilder.one.users({
from: relationBuilder.posts.authorId,
to: relationBuilder.users.id
})
},
users: {
posts: relationBuilder.many.posts()
}
}))
export const db = drizzle(client, { relations })
Use these relation options only when they solve a real schema need:
from and to connect local relation columns to target relation columns.alias is useful when two tables have multiple relations between them.optional: false makes the relation required at the type level; use it only
when the related row is guaranteed to exist.where in relation definitions creates predefined relation filters. It can
filter only columns on the target to table.Use through to expose the target table directly instead of querying a junction
table and mapping it out manually.
export const relations = defineRelations(schema, (relationBuilder) => ({
users: {
groups: relationBuilder.many.groups({
from: relationBuilder.users.id.through(
relationBuilder.usersToGroups.userId
),
to: relationBuilder.groups.id.through(
relationBuilder.usersToGroups.groupId
)
})
},
groups: {
participants: relationBuilder.many.users()
}
}))
const users = await db.query.users.findMany({
with: {
groups: true
}
})
Use predefined filters when a named relation always needs the same target-table filter.
export const relations = defineRelations(schema, (relationBuilder) => ({
groups: {
verifiedUsers: relationBuilder.many.users({
from: relationBuilder.groups.id.through(
relationBuilder.usersToGroups.groupId
),
to: relationBuilder.users.id.through(
relationBuilder.usersToGroups.userId
),
where: {
verified: true
}
})
}
}))
const groups = await db.query.groups.findMany({
with: {
verifiedUsers: true
}
})
Drizzle has two distinct query APIs. Choosing the wrong one causes TypeScript errors or forces awkward query shapes.
Use the relational API (db.query.table.findFirst/findMany) when:
limit, or
nested offset.Use the SQL builder (db.select().from().where()) when:
WHERE conditions dynamically at runtime.count(), sum(), etc.).where takes an objectUse object-shaped filters in RQB v2. A plain property value means equality; use operator objects for non-equality conditions.
const item = await db.query.items.findFirst({
where: {
id
}
})
const approved = await db.query.items.findMany({
where: {
status: 'approved',
price: {
gt: 0
}
}
})
Do not pass SQL helper expressions directly to relational where.
// Wrong in RQB v2: use this shape in the SQL builder instead.
const item = await db.query.items.findFirst({
where: eq(items.id, id)
})
RQB v2 filters support AND, OR, NOT, column operators, RAW, and relation
filters.
import { sql } from 'drizzle-orm'
const users = await db.query.users.findMany({
where: {
AND: [
{
id: {
gt: 10
}
},
{
OR: [
{
email: {
ilike: '%@example.com'
}
},
{
RAW: (table) => sql`${table.lastLogin} is not null`
}
]
},
{
posts: {
title: {
like: 'Release%'
}
}
}
]
}
})
Use RAW only for cases the object operators cannot express, and still route
user input through parameterized sql expressions.
Use columns to restrict selected fields and with to load related rows.
const brand = await db.query.brands.findFirst({
columns: {
id: true,
name: true,
slug: true
},
where: {
slug
},
with: {
items: {
columns: {
id: true,
name: true
},
where: {
status: 'approved'
},
orderBy: {
id: 'asc'
},
limit: 10,
offset: 0,
with: {
category: {
columns: {
name: true,
slug: true
}
}
}
}
}
})
When columns contains both true and false, the false entries are
redundant because the included true fields already define the result shape.
Prefer object-shaped orderBy for normal ordering. Use callback/custom SQL
ordering only when the object form cannot express the requirement.
const posts = await db.query.posts.findMany({
orderBy: {
createdAt: 'desc'
},
with: {
comments: {
orderBy: {
id: 'asc'
}
}
}
})
extrasUse extras for per-row computed fields. Do not put aggregations in extras;
use core queries or an explicit supported subquery pattern such as db.$count
when the project already uses it.
const posts = await db.query.posts.findMany({
extras: {
contentLength: (table, { sql }) => sql<number>`length(${table.content})`
},
with: {
comments: {
extras: {
commentLength: (table, { sql }) => sql<number>`length(${table.content})`
}
}
}
})
Build conditions into an array, then spread into and():
import { and, eq, ilike } from 'drizzle-orm'
const conditions = [
eq(items.status, 'approved')
]
if (categoryId) {
conditions.push(eq(items.categoryId, categoryId))
}
if (search) {
const escaped = search
.replaceAll('%', String.raw`\%`)
.replaceAll('_', String.raw`\_`)
conditions.push(
ilike(items.name, `%${escaped}%`)
)
}
const results = await db
.select({
id: items.id,
name: items.name
})
.from(items)
.where(and(...conditions))
.limit(limit)
.offset((page - 1) * limit)
Always escape user input before passing to ilike() because % and _ are
wildcards in SQL LIKE patterns.
count() returns exactly one row at runtime, but with
noUncheckedIndexedAccess TypeScript still treats indexed access as possibly
undefined. Avoid array destructuring here and read the first row safely.
import { count } from 'drizzle-orm'
const countRows = await db
.select({ total: count() })
.from(items)
.where(
and(...conditions)
)
const total = countRows[0]?.total ?? 0
Run independent queries with Promise.all to avoid sequential round-trips.
const [rows, countRows] = await Promise.all([
db
.select()
.from(items)
.where(
and(...conditions)
)
.limit(limit)
.offset(offset),
db
.select({ total: count() })
.from(items)
.where(
and(...conditions)
)
])
const total = countRows[0]?.total ?? 0
return {
items: rows,
total,
page,
limit
}
import { eq } from 'drizzle-orm'
const results = await db
.select({
brandName: brands.name,
id: items.id,
name: items.name
})
.from(items)
.innerJoin(brands, eq(items.brandId, brands.id))
.where(
eq(items.status, 'approved')
)
development
Plan and drive non-trivial coding work from ambiguous request to scoped implementation and verification. Use when the user asks to plan before coding, plan then implement, split work into iterations or PR-sized tasks, tackle a risky multi-file feature, refactor, migration, or recover after failed work. Do not use for simple one-step edits, commit or PR creation, pure framework/domain conventions, or repo-specific roadmap docs where a more specific planning skill applies.
development
TypeScript coding conventions for writing, reviewing, and refactoring typed code. Use when working on `.ts`, `.tsx`, or files that embed TypeScript such as Vue, Astro, or Svelte components. Also use for TypeScript snippets, typed refactors, and review comments about code organization or function structure.
development
Write and maintain Vitest unit tests for TypeScript code. Use when the user needs unit coverage for utilities, services, or stores, or asks for Vitest-based tests with mocks, spies, and assertions.
development
Create GitHub pull requests from code changes via API or generate PR content in chat. Use when user wants to create/open/submit PR, mentions pull request/PR/merge request/code review, or asks to show/generate/display/output PR content in chat (give me PR, PR to chat, send PR to chat, etc).