Drizzle ORM
Pathrule2 Rules • 3 Memories • 1 Skill
Guardrails for teams using Drizzle ORM as their type-safe SQL layer. It keeps the TypeScript schema as the single source of truth, enforces a generate-then-migrate workflow with drizzle-kit, and steers queries toward inferred types, relations v2, and safe transactions. Use it to stop schema drift and silent N+1 patterns before they ship.
Suggested path map
Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.
Rules
2Schema is the single source of truth/src/db/schemahighstrictChange tables in the TypeScript schema, never in the live database; export inferred types, never hand-write row shapes.
| 1 | Treat the Drizzle TypeScript schema as the only place a table shape is defined. |
| 2 | |
| 3 | - Edit column and table definitions in `src/db/schema`, then run `drizzle-kit generate` to produce the SQL migration. |
| 4 | - Never alter columns directly in the database or hand-edit a generated migration's SQL after it is committed; both create a drift that silently breaks generated types. |
| 5 | - Export inferred types with `typeof users.$inferSelect` and `typeof users.$inferInsert` instead of redeclaring row shapes by hand. Hand-written interfaces fall out of sync whenever a column is added or removed. |
| 6 | - Define foreign keys, indexes, and constraints in the schema so `drizzle-kit` can diff and version them alongside column changes. |
Memories
3Drizzle versions and config baseline (2026)/src/dbCurrent package versions, dialect-based config, and shared db-client conventions.
| 1 | Pin to the current Drizzle line and use the dialect-based config introduced in the 0.40+ releases. |
| 2 | |
| 3 | - Runtime is `drizzle-orm` and the CLI is `drizzle-kit`. The stable line as of mid-2026 is 0.4x; a v1.0.0 release is in progress — confirm the exact pinned version in `package.json` before relying on APIs documented only for v1. |
| 4 | - `drizzle.config.ts` uses `dialect` (`"postgresql"`, `"mysql"`, or `"sqlite"`) plus `schema` and `out` paths; the older `driver` key is removed in the new config format. |
| 5 | - Keep one shared `db` client instance per process. Pass the `db` (or a `tx`) into functions as a parameter rather than importing a singleton inside deeply nested modules, so transaction boundaries stay explicit. |
| 6 | - For Postgres, create the client with `drizzle(pool, { schema })` where `pool` is a `node-postgres` Pool or `postgres` (postgres.js) instance. Passing the schema object enables the relational query API. |
| 7 | |
| 8 | See /src/db for querying patterns and /src/db for relations v2 setup. |
Relations v2: defineRelations and the relational query API/src/dbHow to declare relations once with defineRelations, pass them to drizzle(), and use the with: {} query API without N+1 loops.
| 1 | Relations v2, introduced in Drizzle 0.36+, moves relation declarations out of individual table files and into a centralized definition. Agents frequently write the old per-table `relations()` pattern or skip the relational API entirely and write N+1 loops. |
| 2 | |
| 3 | - Define all relations in a dedicated file (for example `src/db/relations.ts`) using `defineRelations` exported from `drizzle-orm`: |
| 4 | ```ts |
| 5 | import { defineRelations } from 'drizzle-orm'; |
| 6 | import * as schema from './schema'; |
| 7 | |
| 8 | export const relations = defineRelations(schema, (r) => ({ |
| 9 | users: { |
| 10 | posts: r.many.posts({ from: schema.users.id, to: schema.posts.authorId }), |
| 11 | }, |
| 12 | posts: { |
| 13 | author: r.one.users({ from: schema.posts.authorId, to: schema.users.id }), |
| 14 | }, |
| 15 | })); |
| 16 | ``` |
| 17 | - Pass the relations object to `drizzle()` alongside the schema: `const db = drizzle(pool, { schema, relations })`. |
| 18 | - Query nested data with `db.query.users.findMany({ with: { posts: true } })`. This issues a single SQL query (a lateral join), not a loop of per-row queries. The old per-table `relations()` helper is still supported for compatibility but new code should use `defineRelations`. |
| 19 | - For optional or filtered eager loads, pass `{ with: { posts: { where: eq(posts.published, true) } } }` rather than filtering in application code after loading all rows. |
Querying patterns: transactions and prepared statements/src/dbAtomic transactions with savepoints and prepared statements for hot paths.
| 1 | Prefer Drizzle's built-in abstractions for transactions and repeated queries over hand-rolled SQL. |
| 2 | |
| 3 | - Wrap multi-statement writes in `db.transaction(async (tx) => { ... })` and use only `tx` inside the callback. Throwing (or returning a rejected promise) rolls everything back automatically. Nested `db.transaction()` calls on the same `tx` become savepoints. |
| 4 | - For hot paths, build a prepared statement once with `.prepare('name')` and bind values via `sql.placeholder('name')`, then call `.execute({ name: value })`. Prepared statements send only bind parameters on repeated calls and let Postgres skip re-planning. |
| 5 | - Reach for the `sql` template tag for expressions Drizzle has no query builder for. Always pass user input as parameters in the template (`sql`SELECT * FROM users WHERE id = ${userId}``), never as interpolated string concatenation. |
| 6 | - Avoid `db.execute(sql.raw(...))` with unsanitized strings; it bypasses Drizzle's parameter binding and introduces SQL injection. |
Skills
1drizzle-orm-review/rootChecklist to review a Drizzle schema change, migration, and queries before merge.
| 1 | --- |
| 2 | name: drizzle-orm-review |
| 3 | description: Review a Drizzle ORM change before merge. Use on schema modifications, drizzle-kit migrations, relation definitions, query patterns, and transaction logic. |
| 4 | --- |
| 5 | |
| 6 | # Drizzle ORM review |
| 7 | |
| 8 | ## Schema and migrations |
| 9 | - [ ] Schema change lives in `src/db/schema` and the database was not edited directly. |
| 10 | - [ ] A `drizzle-kit generate` migration is committed in `drizzle/` for every schema diff. |
| 11 | - [ ] `drizzle-kit push` is not used against any shared or production database. |
| 12 | - [ ] Destructive changes (drop or retype a column) have tested reverse SQL and a staging plan. |
| 13 | - [ ] Row types come from `$inferSelect` / `$inferInsert`, not hand-written interfaces. |
| 14 | |
| 15 | ## Config and client |
| 16 | - [ ] `drizzle.config.ts` sets `dialect`, `schema`, and `out`; no legacy `driver` key. |
| 17 | - [ ] One shared `db` client instance per process; the `db` or `tx` is passed into functions, not re-imported from a singleton. |
| 18 | |
| 19 | ## Relations and queries |
| 20 | - [ ] Relations use `defineRelations` in a centralized file and are passed to `drizzle(pool, { schema, relations })`; no per-table `relations()` on new code. |
| 21 | - [ ] Nested reads use `db.query.<table>.findMany({ with: { ... } })`, not per-row loops. |
| 22 | - [ ] Multi-statement writes run inside `db.transaction` using only `tx` internally. |
| 23 | - [ ] Hot-path queries use `.prepare()` with `sql.placeholder`; user input stays parameterized and `sql.raw` is not used with unsanitized input. |
Why this pattern
AI agents drift Drizzle schemas out of sync with the database and write untyped, N+1-prone queries.
Built for Backend and full-stack teams running Drizzle ORM on Postgres, MySQL, or SQLite..
Keeps your assistant from:
- Editing the database directly instead of regenerating migrations from the schema
- Hand-writing column types instead of inferring them with $inferSelect and $inferInsert
- Issuing per-row queries in loops instead of using relations or a single batched query
- License
- Apache-2.0
- Version
- 1.0.0
- Updated
- 2026-06-09