Drizzle ORM
How do you keep type-safe queries fast, consistent, and auditable?
Drizzle is the ORM layer. The repository standards define WHAT every repo must do. This page defines HOW in Drizzle.
Prepared Statements
Use .prepare() on hot-path reads only. Fixed-shape methods where the query plan doesn't change between calls.
// Hot path — prepare once, execute many
const findById = db
.select()
.from(contacts)
.where(eq(contacts.id, sql.placeholder("id")))
.prepare("contacts_find_by_id");
// Cold path — dynamic filters, no prepare
const search = (filters: ContactFilters) => db.select().from(contacts).where(buildWhere(filters));
Rule: If the query shape varies by caller input (optional filters, dynamic joins), skip .prepare(). The plan cache won't help.
Transaction Executor
Every mutation method accepts a db | tx parameter. Callers compose transactions; repositories don't own them.
type Executor = PostgresJsDatabase | PostgresJsTransaction;
async function createContact(executor: Executor, data: NewContact) {
return executor.insert(contacts).values(data).returning();
}
// Caller composes
await db.transaction(async (tx) => {
const contact = await createContact(tx, contactData);
await createActivity(tx, { contactId: contact.id, type: "created" });
});
Rule: Never db.transaction() inside a repository method. The caller decides scope.
Error Mapping
DB constraint violations map to typed domain results. No raw Postgres errors leak past the repository boundary.
type RepoResult<T> =
| { success: true; data: T }
| { success: false; errors: DomainError[] };
async function createContact(
executor: Executor,
data: NewContact,
): Promise<RepoResult<Contact>> {
try {
const [row] = await executor.insert(contacts).values(data).returning();
return { success: true, data: mapToContact(row) };
} catch (err) {
if (isUniqueViolation(err)) {
return { success: false, errors: [{ code: 'DUPLICATE', field: 'email' }] };
}
throw err; // Unknown errors still throw
}
}
Rule: Catch known constraint errors. Re-throw unknowns. Never swallow.
Benchmark Harness
Measure repository performance against runtime SLOs.
| Component | Purpose |
|---|---|
| Seeded test DB | Deterministic dataset, reset per suite |
| Warm-up round | Discard first N runs (JIT, cache priming) |
| p50/p95/p99 | Capture percentile distribution |
| Assertion | Fail CI if p95 exceeds SLO |
// Pseudocode — benchmark runner shape
const results = await benchmarkMethod(findById, { id: seedIds }, { runs: 1000, warmup: 50 });
assert(results.p95 < 10, `findById p95 ${results.p95}ms exceeds 10ms SLO`);
Migration Discipline
Schema changes are reviewed artifacts. The SQL file is the contract between intent and database state.
Workflow
schema change → drizzle-kit generate → review SQL → commit → CI validates → staging migrates → prod migrates
| Rule | Why |
|---|---|
| Generate locally, migrate in CI | Production never generates — it applies committed SQL only |
| One migration per feature branch | Trail of intermediate files = noisy review, risky rollback |
| Review both schema diff AND SQL diff | Schema shows intent. SQL shows execution. Mismatch = bug |
| Staging migrates before app deploy | DAL and seeds must never run against stale schema |
| Never edit committed migration files | Editing breaks the journal hash chain. Create a new migration instead |
Naming
drizzle-kit timestamps automatically. Add intent to the --name flag for human readability during review and incident response.
npx drizzle-kit generate --name=add_governance_nav_items
Result: 0042_add_governance_nav_items.sql — the timestamp orders, the name explains.
Seed Separation
Migrations own structure. Seeds own bootstrap data. Keep them separate unless the data is required for schema integrity (enum tables, lookup rows referenced by foreign keys).
| Layer | Owns | Example |
|---|---|---|
| Migration | Tables, indexes, constraints, enum types | CREATE TABLE governance_nav_items |
| Seed | Default rows, test fixtures, bootstrap data | Insert default nav items for each org |
Rollback
Drizzle has no first-party rollback. Plan for forward-fix.
| Scenario | Action |
|---|---|
| Bad migration in staging | Write a corrective migration, push, re-deploy |
| Bad migration in prod | Write corrective migration. If data loss risk: restore from backup first |
| Need to undo a column add | New migration: ALTER TABLE DROP COLUMN |
| Need to undo a table create | New migration: DROP TABLE (only if no data written) |
Rule: Every destructive migration (drop column, drop table, alter type) gets a manual review gate in CI. Automated deploy for additive only.
CI Checks
| Check | Tool | Blocks Deploy If |
|---|---|---|
| Migration files committed | Git diff | Generated SQL not in the commit |
| Schema and SQL in sync | drizzle-kit check | Schema changed but no migration generated |
| No edited migrations | Hash comparison | Committed migration file modified after merge |
| Destructive change flagged | Custom script | DROP, ALTER ... DROP, type changes without review |
Deploy Sequence
1. Run migrations (drizzle-kit migrate)
2. Run seeds (if new bootstrap data)
3. Deploy application
4. Run integration/E2E tests
5. Release traffic
Never deploy the app before migrations complete. Never run seeds before migrations.
Anti-Patterns
| Pattern | Problem | Fix |
|---|---|---|
drizzle-kit push in production | Skips migration files, no audit trail | Use generate + migrate always |
Editing a committed .sql file | Breaks journal integrity | Write a new corrective migration |
| Multiple migrations per feature | Noisy review, fragile rollback | Squash before merge or generate once at the end |
| Seeds inside migration files | Couples data to structure | Separate seed scripts |
Running generate in CI/prod | Non-deterministic — depends on local schema state | Generate locally, commit the output |
AST Analyzer
Static analysis catches consistency drift before runtime. Two paths:
| Approach | Tool | Catches |
|---|---|---|
| Script | ts-morph | Missing methods, section ordering, any in sigs |
| Nx plugin | Custom | Per-project scoring in CI pipeline |
Neither exists yet. The repository standards define the 10 rules the analyzer would enforce.
Links
- Drizzle GitHub — Source and documentation
- Drizzle Docs — Official documentation
Context
- Data Engineering — Repository standards this page implements
- Repository Patterns — Patterns, anti-patterns, and generator checklist from production review
- Repository Quality — Scorecard that grades runtime + consistency
- Hexagonal Architecture — Why repositories sit at the adapter boundary
Questions
When every repository method accepts db | tx, what prevents callers from composing transactions that span too many aggregates?
- If prepared statements only help fixed-shape queries, what percentage of your hot path is actually fixed-shape?
- At what codebase size does a ts-morph script become too slow — and is that the signal to build the Nx plugin?
- If migrations are reviewed artifacts but seeds are not, what catches a bad seed that corrupts bootstrap data?
- When a destructive migration passes review but breaks a downstream service, is the gap in the review checklist or the dependency map?