Skip to main content

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.

ComponentPurpose
Seeded test DBDeterministic dataset, reset per suite
Warm-up roundDiscard first N runs (JIT, cache priming)
p50/p95/p99Capture percentile distribution
AssertionFail 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
RuleWhy
Generate locally, migrate in CIProduction never generates — it applies committed SQL only
One migration per feature branchTrail of intermediate files = noisy review, risky rollback
Review both schema diff AND SQL diffSchema shows intent. SQL shows execution. Mismatch = bug
Staging migrates before app deployDAL and seeds must never run against stale schema
Never edit committed migration filesEditing 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).

LayerOwnsExample
MigrationTables, indexes, constraints, enum typesCREATE TABLE governance_nav_items
SeedDefault rows, test fixtures, bootstrap dataInsert default nav items for each org

Rollback

Drizzle has no first-party rollback. Plan for forward-fix.

ScenarioAction
Bad migration in stagingWrite a corrective migration, push, re-deploy
Bad migration in prodWrite corrective migration. If data loss risk: restore from backup first
Need to undo a column addNew migration: ALTER TABLE DROP COLUMN
Need to undo a table createNew 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

CheckToolBlocks Deploy If
Migration files committedGit diffGenerated SQL not in the commit
Schema and SQL in syncdrizzle-kit checkSchema changed but no migration generated
No edited migrationsHash comparisonCommitted migration file modified after merge
Destructive change flaggedCustom scriptDROP, 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

PatternProblemFix
drizzle-kit push in productionSkips migration files, no audit trailUse generate + migrate always
Editing a committed .sql fileBreaks journal integrityWrite a new corrective migration
Multiple migrations per featureNoisy review, fragile rollbackSquash before merge or generate once at the end
Seeds inside migration filesCouples data to structureSeparate seed scripts
Running generate in CI/prodNon-deterministic — depends on local schema stateGenerate locally, commit the output

AST Analyzer

Static analysis catches consistency drift before runtime. Two paths:

ApproachToolCatches
Scriptts-morphMissing methods, section ordering, any in sigs
Nx pluginCustomPer-project scoring in CI pipeline

Neither exists yet. The repository standards define the 10 rules the analyzer would enforce.

Context

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?