Drizzle: relation does not exist — типичные причины
Запускаешь приложение, делаешь первый запрос, и Postgres отвечает:
error: relation "users" does not existНа стороне Drizzle никаких компиляционных ошибок нет: схема валидная, типы есть, IDE подсвечивает поля. А база об этой таблице ничего не знает.
В моей практике эта ошибка ловится на одних и тех же повторяющихся причинах. Покажу их в порядке убывания «обычно это вот это».
1. Миграции не применены
Самый частый случай. Ты обновил схему в schema.ts, сгенерировал миграцию, но не накатил её на БД. Drizzle Kit сгенерил drizzle/0001_xxx.sql — файл лежит в репозитории, но на бэкенде ничего не выполнено.
Лечится одной командой:
pnpm drizzle-kit migrateИли через программный API (если ты гоняешь миграции из кода):
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);
await migrate(db, { migrationsFolder: './drizzle' });На моём типичном проекте миграции вызываются на старте приложения. Если на сервере БД новая, а миграции на ней не запускались — конечно, relation does not exist.
2. Подключилась не та база
Бывает, ты в development-режиме сидишь на одной БД, а в проде или staging — на другой, где схемы ещё нет. DATABASE_URL переключился незаметно — например, через .env.production.
Быстрая проверка:
psql "$DATABASE_URL" -c "\dt"Эта команда покажет таблицы в текущей подключённой БД. Если там пусто или нет нужной — ты подключился не туда либо миграции не накатывались.
3. Search path и схемы
Postgres умеет несколько схем (public, app, что угодно). По умолчанию таблицы создаются в public. Но если ты в schema.ts сделал так:
import { pgSchema, pgTable } from 'drizzle-orm/pg-core';
export const appSchema = pgSchema('app');
export const users = appSchema.table('users', {
id: serial('id').primaryKey(),
email: text('email').notNull(),
});Drizzle сгенерит миграцию, которая создаст app.users. Если ты потом подключаешься с search_path = public и пишешь запрос вручную «from users» — Postgres не найдёт. И Drizzle тоже сообщит про не существующую relation, если там что-то перепутано.
Проверка:
SHOW search_path;
SELECT schemaname, tablename FROM pg_tables WHERE tablename = 'users';Решение — либо явно указывать схему в Drizzle (appSchema.table), либо менять search_path в коннекте.
4. Опечатка в имени таблицы
Postgres различает регистр идентификаторов в зависимости от того, заквочены они или нет. users и "Users" — две разные таблицы. Если ты случайно создал таблицу как "Users" (большие буквы в кавычках) и потом запрашиваешь users — будет «does not exist».
Drizzle сам не делает таких сюрпризов: он использует имена ровно так, как ты прописал в схеме. Но если миграция уже была применена руками или другой ORM создавала таблицы — может прилететь.
Проверка:
SELECT tablename FROM pg_tables WHERE schemaname = 'public';Если видишь Users вместо users — нужно либо переименовать, либо в Drizzle прописать имя точно так же.
5. Migration journal в Drizzle разъехался
Drizzle Kit ведёт «журнал» применённых миграций в таблице __drizzle_migrations. Если эта таблица говорит, что миграция уже применена, а сама таблица в БД отсутствует — может быть рассинхрон. Например, ты руками дропнул таблицу или восстановил БД из старого бэкапа.
Симптом тонкий: drizzle-kit migrate ничего не делает (потому что считает, что всё накатано), а в БД таблиц нет. Лечится либо ручным удалением соответствующих записей из __drizzle_migrations и повторным migrate, либо переподходом — снеси БД и накати с нуля, если это dev.
6. Drizzle подключён к БД, в которой нет миграций (multi-tenant)
На проектах с несколькими БД на одного пользователя я видел такое: код работает с БД tenant_42, а миграции накатывались только на tenant_main. Каждый новый tenant требует своей пробежки миграций, и если этого этапа нет — у нового арендатора пустая БД, и при первом запросе вылетает «relation does not exist».
Решение — на каждый новый tenant запускать migrate прежде, чем приложение начнёт ходить в эту БД.
7. Транзакции и checkpoint
Совсем редкий, но выползающий случай: ты руками в одной транзакции делаешь CREATE TABLE, потом из другой пытаешься сделать SELECT, а транзакция ещё не закоммичена. Другие коннекты не увидят таблицу, пока не commit. На обычной разработке это не страшно (миграции у Drizzle коммитят), но если интегрируешь Drizzle с какими-то внешними миграционными скриптами — стоит держать в голове.
Алгоритм диагностики, которым я пользуюсь
- Подключиться к нужной БД
psqlи сделать\dt. Видишь нужную таблицу — продолжай. Нет — мигрировал не туда. - Проверить
DATABASE_URL, к которой реально подключается приложение. Не поленись напечатать в логах при старте (без пароля!). - Если таблица существует под другим именем (регистр, схема) — поправить либо схему Drizzle, либо БД.
- Если миграция в журнале есть, а таблицы нет — синхронизировать.
Профилактика
На всех своих проектах я делаю миграции обязательной частью старта приложения. Это лишние 50 миллисекунд на загрузку, но ноль шансов забыть мигрировать на проде:
// src/server/init.ts
import { migrate } from 'drizzle-orm/node-postgres/migrator';
export async function initDb() {
await migrate(db, { migrationsFolder: './drizzle' });
}В CI у меня шаг «прогнать миграции на тестовой БД» отдельной job-ой — если что-то сломалось в schema.ts, упадёт ещё до деплоя.
Сообщение «relation does not exist» в 95% случаев — это «ты не накатил миграцию» либо «подключился не туда». Остальные 5% — про регистр, схемы и multi-tenant. Главное — не паниковать и идти по списку.