Fix: Drizzle ORM Not Working — Schema Out of Sync, Relation Query Fails, or Migration Error
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Drizzle ORM issues — schema definition, drizzle-kit push vs migrate, relation queries with, transactions, type inference, and common PostgreSQL/MySQL configuration problems.
The Problem
Drizzle queries return empty results or fail despite data existing in the database:
const users = await db.select().from(usersTable);
// Returns [] — but table has rowsOr drizzle-kit push fails with a type mismatch:
Error: column "created_at" is of type timestamp without time zone but expression is of type textOr a relation query throws:
const result = await db.query.users.findMany({
with: { posts: true }
});
// Error: Cannot read properties of undefined (reading 'posts')Or a migration fails partway through and leaves the schema inconsistent:
Error: relation "users" already existsWhy This Happens
Drizzle ORM has a strict relationship between TypeScript schema definitions and the actual database. The library deliberately stays close to SQL semantics — it does not introspect your database at runtime, it does not auto-generate relations, and it does not hide the dialect from you. That design produces sharper type inference than Prisma and faster cold starts, but it makes mismatches between your TypeScript schema and the actual tables fail in confusing ways.
The most common cause is divergence between code and database. You add a column in the database, forget to reflect it in pgTable, and queries silently return objects without that field. Or you generate a migration, push it, then hand-edit the table later — and drizzle-kit no longer agrees with what is actually deployed. Unlike Prisma, which forces a single source of truth via the schema file plus an introspection step, Drizzle assumes you and your migrations are the source of truth at all times.
The second cluster of failures comes from the two different query APIs. The SQL-builder API (db.select().from(table)) does not need relations at all and works without passing schema to drizzle(). The relational query API (db.query.users.findMany({ with: { posts: true } })) requires both relations() definitions and a schema argument. Mixing the two and forgetting the schema argument is the single most reported “Drizzle is broken” issue on GitHub.
- Schema definition is the source of truth — Drizzle doesn’t auto-discover your database schema. If your TypeScript schema doesn’t match the actual table, queries fail or return wrong types silently.
- Relations must be explicitly defined — unlike Prisma which auto-generates relations, Drizzle requires you to define
relations()separately from the table definition. Without it,with: { ... }queries fail. drizzle-kit pushvsdrizzle-kit migrate—pushapplies schema changes directly (good for development),migrateruns generated SQL migration files (required for production). Mixing them can cause conflicts.- Table name in schema must match the actual database table —
pgTable('users', ...)creates a schema bound to theuserstable. If the table is nameduserin the database, all queries silently target the wrong table.
Fix 1: Define Schema Correctly
The schema file is the foundation — every column must match the actual database:
// db/schema.ts — PostgreSQL example
import {
pgTable, serial, text, varchar, integer,
boolean, timestamp, uuid, pgEnum
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Enum must exist in PostgreSQL too
export const roleEnum = pgEnum('role', ['user', 'admin', 'moderator']);
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 255 }).notNull(),
email: text('email').notNull().unique(),
role: roleEnum('role').default('user').notNull(),
isActive: boolean('is_active').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: uuid('author_id').notNull().references(() => users.id, {
onDelete: 'cascade',
}),
publishedAt: timestamp('published_at'),
});
// Relations — required for .query API with `with`
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
// MySQL example
import { mysqlTable, int, varchar } from 'drizzle-orm/mysql-core';
export const mysqlUsers = mysqlTable('users', {
id: int('id').primaryKey().autoincrement(),
name: varchar('name', { length: 255 }).notNull(),
});Fix 2: Configure Drizzle Client Correctly
Connect to the database with the right driver and pass your schema:
// db/index.ts — PostgreSQL with node-postgres
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30000,
});
export const db = drizzle(pool, { schema });
// 'schema' is required for the .query API (relations + type inference)
// PostgreSQL with postgres.js (alternative driver)
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const queryClient = postgres(process.env.DATABASE_URL!);
export const db = drizzle(queryClient, { schema });
// SQLite with better-sqlite3
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
const sqlite = new Database('sqlite.db');
export const db = drizzle(sqlite, { schema });
// MySQL
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import * as schema from './schema';
const connection = await mysql.createConnection(process.env.DATABASE_URL!);
export const db = drizzle(connection, { schema });drizzle.config.ts — required for migrations:
import type { Config } from 'drizzle-kit';
export default {
schema: './db/schema.ts',
out: './drizzle', // Where migration files are stored
dialect: 'postgresql', // 'postgresql' | 'mysql' | 'sqlite'
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;Fix 3: Use Relations with the Query API
The .query API (with with) requires both schema relations and passing the schema to drizzle():
// WRONG — schema not passed to drizzle()
const db = drizzle(pool); // No schema!
const result = await db.query.users.findMany({ with: { posts: true } });
// Error: Cannot read properties of undefined
// CORRECT — pass schema to drizzle()
import * as schema from './schema';
const db = drizzle(pool, { schema });
// Now .query API works
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true, // Include all posts
},
});
// Select specific columns
const usersWithPostTitles = await db.query.users.findMany({
columns: {
id: true,
name: true,
email: true,
// password: false — exclude sensitive fields
},
with: {
posts: {
columns: { id: true, title: true },
where: (posts, { isNull }) => isNull(posts.publishedAt), // Filter
orderBy: (posts, { desc }) => [desc(posts.id)],
limit: 5,
},
},
where: (users, { eq }) => eq(users.isActive, true),
orderBy: (users, { asc }) => [asc(users.name)],
limit: 20,
offset: 0,
});
// Nested relations
const result = await db.query.posts.findFirst({
with: {
author: {
with: {
// posts: true ← Don't nest back — creates circular data
},
columns: { name: true, email: true },
},
},
where: (posts, { eq }) => eq(posts.id, 1),
});Fix 4: Run Migrations Correctly
Use drizzle-kit push for development, drizzle-kit migrate for production:
# Generate migration files from schema changes
npx drizzle-kit generate
# Apply pending migrations (production — uses migration files)
npx drizzle-kit migrate
# Push schema directly to database (development only — no migration files)
npx drizzle-kit push
# Inspect the current database schema
npx drizzle-kit introspect
# View migration status
npx drizzle-kit studio # Opens browser UIProgrammatic migrations (run at app startup):
// db/migrate.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
async function runMigrations() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);
console.log('Running migrations...');
await migrate(db, { migrationsFolder: './drizzle' });
console.log('Migrations complete');
await pool.end();
}
runMigrations().catch(console.error);Handle migration conflicts:
# If "relation already exists" — the migration was partially applied
# Option 1: Manually mark migration as applied
# Insert into drizzle migration table:
INSERT INTO drizzle.__drizzle_migrations (hash, created_at)
VALUES ('your_migration_hash', NOW());
# Option 2: Drop and recreate (development only)
npx drizzle-kit push --force
# Option 3: Generate a new migration to fix the state
# Modify schema to reflect current DB state, then generate
npx drizzle-kit generateFix 5: Common Query Patterns
import { eq, and, or, like, gte, lte, inArray, isNull, sql } from 'drizzle-orm';
// WHERE conditions
const activeAdmins = await db.select()
.from(users)
.where(
and(
eq(users.isActive, true),
eq(users.role, 'admin')
)
);
// OR condition
const results = await db.select()
.from(users)
.where(
or(
like(users.email, '%@company.com'),
eq(users.role, 'admin')
)
);
// Date range
const recentPosts = await db.select()
.from(posts)
.where(
gte(posts.publishedAt, new Date('2024-01-01'))
);
// IN clause
const specificUsers = await db.select()
.from(users)
.where(inArray(users.id, ['uuid-1', 'uuid-2', 'uuid-3']));
// JOIN
const postsWithAuthors = await db.select({
postId: posts.id,
title: posts.title,
authorName: users.name,
}).from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(isNull(posts.publishedAt));
// Aggregation
const postCounts = await db.select({
userId: users.id,
userName: users.name,
postCount: sql<number>`count(${posts.id})::int`,
}).from(users)
.leftJoin(posts, eq(users.id, posts.authorId))
.groupBy(users.id, users.name)
.having(sql`count(${posts.id}) > 0`);
// Insert and return
const [newUser] = await db.insert(users)
.values({ name: 'Alice', email: '[email protected]' })
.returning(); // Returns the inserted row with generated id, timestamps
// Update
const [updated] = await db.update(users)
.set({ name: 'Alicia', updatedAt: new Date() })
.where(eq(users.id, 'user-uuid'))
.returning();
// Upsert (INSERT ON CONFLICT)
await db.insert(users)
.values({ id: 'user-uuid', name: 'Alice', email: '[email protected]' })
.onConflictDoUpdate({
target: users.email,
set: { name: sql`excluded.name`, updatedAt: new Date() },
});Fix 6: Transactions
Group related operations in a transaction to ensure atomicity:
// Simple transaction
const result = await db.transaction(async (tx) => {
const [order] = await tx.insert(orders)
.values({ userId: 'user-1', total: 99.99 })
.returning();
await tx.insert(orderItems).values([
{ orderId: order.id, productId: 'prod-1', quantity: 2 },
{ orderId: order.id, productId: 'prod-2', quantity: 1 },
]);
await tx.update(inventory)
.set({ stock: sql`${inventory.stock} - 2` })
.where(eq(inventory.productId, 'prod-1'));
return order;
});
// Transaction with rollback on error
try {
await db.transaction(async (tx) => {
const [sender] = await tx.select().from(accounts)
.where(eq(accounts.id, fromId))
.for('update'); // Row-level lock
if (sender.balance < amount) {
tx.rollback(); // Explicit rollback — throws TxRollbackError
}
await tx.update(accounts)
.set({ balance: sql`${accounts.balance} - ${amount}` })
.where(eq(accounts.id, fromId));
await tx.update(accounts)
.set({ balance: sql`${accounts.balance} + ${amount}` })
.where(eq(accounts.id, toId));
});
} catch (e) {
if (e instanceof TransactionRollbackError) {
console.log('Insufficient funds');
} else {
throw e;
}
}Drizzle vs Prisma vs Kysely vs TypeORM vs MikroORM
The TypeScript ORM landscape has split into four philosophies, and most “Drizzle not working” problems disappear once you understand which philosophy you actually want. Picking the wrong tool, then fighting it, is a much more common failure mode than any single bug.
Drizzle is SQL-first. You write SQL-shaped TypeScript that maps almost one-to-one to the query the database receives. There is no query engine binary, no proxy process, no Rust runtime — drizzle-orm is a thin TypeScript wrapper. Type inference is derived from your schema with InferSelectModel and InferInsertModel, so the returned row type changes the instant you adjust a column. The trade-off is that Drizzle gives you very little hand-holding: if you want a relation, you write relations(); if you want a nested include, you opt into the .query API; if you want migrations, you choose between push and generate + migrate. None of this is automatic.
Prisma is schema-first. You write schema.prisma in a custom DSL, run prisma generate to produce a client, and queries go through a Rust query engine. The DX is smoother for CRUD (prisma.user.findMany({ include: { posts: true } }) just works), and relations are inferred from the schema file. The cost is a heavy generated client, slower cold starts on serverless, and an N+1 risk in the relational loader. See Fix: Prisma Migration Failed for the version-mismatch issues that bite the most.
Kysely is query-builder-only. It is closer to a typed knex than to a real ORM — no relations, no migrations, just a strongly typed builder over a Database interface you maintain. If Drizzle feels too magical (because of relations() or the .query API), Kysely strips even those abstractions away. Many teams pair kysely-codegen with raw SQL migrations and never look back.
TypeORM and MikroORM are nominal/decorator-driven. Both lean on the classic Java/C# @Entity decorator pattern with a Unit-of-Work pattern (MikroORM more strictly than TypeORM). They give you change-tracking, lazy loading, and an EntityManager. The TypeScript inference is weaker than Drizzle’s because the schema lives in runtime decorators rather than literal types, and the metadata layer makes serverless cold starts heavier.
| ORM | Style | Relations | Migrations | Cold start | Type inference |
|---|---|---|---|---|---|
| Drizzle | SQL-builder + .query | Explicit relations() | kit generate + migrate or push | Very fast | Excellent (literal types) |
| Prisma | Schema-first | Auto from .prisma | prisma migrate dev/deploy | Slow (Rust engine) | Excellent (generated client) |
| Kysely | Pure query builder | Manual joins | None (BYO) | Very fast | Excellent if Database is accurate |
| TypeORM | Decorator/Entity | Auto via decorators | typeorm migration:generate | Medium | Weak (metadata-based) |
| MikroORM | Decorator/Unit-of-Work | Auto + flush() | mikro-orm migration:create | Medium | Medium |
If your bug is “relations come back undefined,” you are on Drizzle and should pass { schema } to drizzle(). If your bug is “the generated client is huge in my Cloudflare Worker,” you are on Prisma and should switch to Drizzle or Kysely. If your bug is “migrations randomly drop columns,” you are on TypeORM’s synchronize: true and should turn it off and use generated migrations the way Drizzle’s generate + migrate workflow does. Knowing which tool you are fighting cuts hours off most debugging sessions.
Still Not Working?
Column type mismatch between schema and database — if the TypeScript schema says integer but the database column is bigint, queries may return wrong types or fail. Use drizzle-kit introspect to generate a schema from your existing database, then compare it to your hand-written schema. This is the most reliable way to ensure they match.
Timestamps returning strings instead of Date objects — by default, timestamp in Drizzle returns JavaScript Date objects, but the pg driver may return strings depending on the types configuration. Ensure you’re using the correct import (timestamp() from drizzle-orm/pg-core) and check pg’s type parsers. Adding .withTimezone() changes the behavior:
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow(),drizzle-kit push doesn’t detect changes — Drizzle kit compares your TypeScript schema to the database schema. If you’ve manually altered the database table outside of Drizzle, the snapshot may be out of sync. Delete the drizzle/meta/_journal.json and snapshot files, then run drizzle-kit push again (development only).
Serverless connection exhaustion under load — Pool from pg keeps long-lived TCP connections, which is fine on a long-running Node process but disastrous on Vercel or Cloudflare Workers where every cold start opens new sockets. Switch to drizzle-orm/neon-http, drizzle-orm/planetscale-serverless, or drizzle-orm/postgres-js with max: 1 and idle_timeout: 20. If you keep seeing too many clients already after migrating, your provider’s pooler (e.g., PgBouncer on transaction mode) may also be blocking prepared statements — pass prepare: false to postgres-js.
AsyncLocalStorage context lost inside db.transaction() — if you wrap your Drizzle queries in request-scoped context (for tenant IDs or tracing), the callback passed to db.transaction() runs inside the driver’s own promise chain. Capture the context value before the transaction and re-pass it, or use the driver’s hooks rather than wrapping the whole callback in AsyncLocalStorage.run(). This is the same gotcha that affects Prisma’s $transaction callback.
Boolean and JSON columns deserialize differently across drivers — node-postgres returns boolean as true/false, but postgres-js returns true/false only when types is configured. For jsonb, postgres-js parses to a JS object automatically, but mysql2 returns a string you must JSON.parse. If a field “looks wrong” only on one driver, check the driver’s type-parser docs before assuming Drizzle is at fault.
For related database issues, see Fix: Prisma Migration Failed, Fix: PostgreSQL Relation Does Not Exist, Fix: Kysely Not Working, and Fix: SQLAlchemy Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Prisma Transaction Error — Transaction Already Closed or Rolled Back
How to fix Prisma transaction errors — interactive transactions vs $transaction array, error handling and rollback, nested transactions, timeout issues, and isolation levels.
Fix: TypeORM QueryFailedError and Entity Not Found
How to fix TypeORM QueryFailedError, entity not found errors, relation issues, missing migrations, and connection configuration problems in Node.js and NestJS applications.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Neon Database Not Working — Connection Timeout, Branching Errors, or Serverless Driver Issues
How to fix Neon Postgres issues — connection string setup, serverless HTTP driver vs TCP, database branching, connection pooling, Drizzle and Prisma integration, and cold start optimization.