PostgreSQL

Drizzle db.query drops pgSchema: relation does not exist

Get db.query.findMany() and findFirst() reading your custom Postgres schema again: which drizzle-orm versions drop the schema, why, and three tested ways out.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

On drizzle-orm 0.39.1 up to 0.40.1, a relational query such as db.query.user.findMany() on a table declared with pgSchema('app_one') builds select "id", "name" from "user", without the schema, and PostgreSQL answers relation "user" does not exist. It happens when the table's key in your Drizzle schema object is the same as its database name. db.select().from(user) is not affected. Upgrade to drizzle-orm 0.41.0 or later, whose release notes list the fix. If you cannot upgrade yet, give the table a key that differs from its database name (appUser for the user table), which makes Drizzle write the full schema-qualified name.

The report is drizzle-orm issue #4060, opened on drizzle-orm 0.39.1 and drizzle-kit 0.30.4. Its schema declares the table inside a custom Postgres schema:

ts
export const user = pgSchema('app_one').table('user', {
  id: integer('id').generatedAlwaysAsIdentity().primaryKey(),
  name: text('name'),
});

db.query.user.findMany() then runs this SQL, which looks for user on the search path instead of in app_one:

sql
select "id", "name" from "user"

A second reporter in the same thread hit it on drizzle 0.40.0 with a table named paypal in a schema named temp, on Bun 1.2.4; their driver reported it as a PostgresError:

plaintext
relation "paypal" does not exist
PostgresError: relation "paypal" does not exist

With pg, the reproduction below got the same message as a DatabaseError.

Which versions build the wrong SQL#

The behaviour was reproduced on 20 October 2026 with the official postgres:16-alpine image (PostgreSQL 16.15), node:22-alpine (Node v22.23.3) and pg 8.13.1. The database holds one table, app_one."user", with one row. The script declares that table twice: once under the key user (the case from the issue) and once under the key appUser, then prints the SQL from .toSQL() and runs both relational queries.

The broken versions, 0.39.1 and 0.40.1:

text
--- drizzle-orm 0.39.1
select().from(user)    : select "id", "name" from "app_one"."user"
query.user.findMany()  : select "id", "name" from "user"
query.appUser.findMany(): select "id", "name" from "app_one"."user" "appUser"
query.user.findMany() -> DatabaseError: relation "user" does not exist
query.appUser.findMany() -> 1 row(s)
text
--- drizzle-orm 0.40.1
select().from(user)    : select "id", "name" from "app_one"."user"
query.user.findMany()  : select "id", "name" from "user"
query.appUser.findMany(): select "id", "name" from "app_one"."user" "appUser"
query.user.findMany() -> DatabaseError: relation "user" does not exist
query.appUser.findMany() -> 1 row(s)

The version just before, 0.39.0, and the fixed 0.41.0:

text
--- drizzle-orm 0.39.0
select().from(user)    : select "id", "name" from "app_one"."user"
query.user.findMany()  : select "id", "name" from "app_one"."user"
query.appUser.findMany(): select "id", "name" from "app_one"."user" "appUser"
query.user.findMany() -> 1 row(s)
query.appUser.findMany() -> 1 row(s)
text
--- drizzle-orm 0.41.0
select().from(user)    : select "id", "name" from "app_one"."user"
query.user.findMany()  : select "id", "name" from "app_one"."user" "user"
query.appUser.findMany(): select "id", "name" from "app_one"."user" "appUser"
query.user.findMany() -> 1 row(s)
query.appUser.findMany() -> 1 row(s)

drizzle-orm 0.38.4, the version two commenters on the issue went back to or stayed on, behaves like 0.39.0. drizzle-orm 0.45.4, the latest tag on npm on 20 October 2026, behaves like 0.41.0. Three facts follow from these runs: the core query builder (db.select().from(user)) was right on every version; the relational query lost the schema only when its key matched the table name; and a key that differs from the name worked even on the broken versions.

Why it happens#

Two changes, one release apart, produced the bug.

0.39.1 stopped prefixing aliases with their schema. The 0.39.1 release notes list "Fixed sql builder prefixing aliased views and tables with their schema". In the source, the SQL builder now writes only the bare name when a table object is an alias (sql.ts at 0.39.1, line 184):

diff
 if (is(chunk, Table)) {
   const schemaName = chunk[Table.Symbol.Schema];
   const tableName = chunk[Table.Symbol.Name];
   return {
-    sql: schemaName === undefined
+    sql: schemaName === undefined || chunk[IsAlias]
       ? escapeName(tableName)
       : escapeName(schemaName) + '.' + escapeName(tableName),

That is correct for an alias that appears in a column reference ("appUser"."id"), but it means the FROM clause must write the full name itself.

The FROM clause only did that when the alias had a different name. The Postgres dialect writes "schema"."table" "alias" in a separate function, and on 0.39.1 to 0.40.1 it decided whether a table was an alias by comparing the alias name with the original table name. Relational Queries v1 alias the top-level table with its key in your schema object (query.ts at 0.40.1, line 124), and that key comes from iterating the object you pass as schema to drizzle() (relations.ts at 0.40.1, line 438). With import * as schema from './schema', the key is the export name. When the key is user and the table is user, both names are equal, the dialect treats the alias as a plain table, and the SQL builder then prints the alias without its schema. That gives from "user".

0.41.0 checks the alias flag instead of the names. The 0.41.0 release notes list "Fixed custom schema tables querying in RQBv1: #4060", shipped in the bundled pull request #4293. The change in the dialect, from the 0.40.1 to 0.41.0 comparison:

diff
-		if (is(table, Table) && table[Table.Symbol.OriginalName] !== table[Table.Symbol.Name]) {
+		if (is(table, Table) && table[Table.Symbol.IsAlias]) {
 			let fullName = sql`${sql.identifier(table[Table.Symbol.OriginalName])}`;
 			if (table[Table.Symbol.Schema]) {
 				fullName = sql`${sql.identifier(table[Table.Symbol.Schema]!)}.${fullName}`;

That is why 0.41.0 prints from "app_one"."user" "user": the table is aliased to its own name, and the schema is back.

Tables declared with pgTable live in the default schema and have no schema name to lose, so the bug only shows with pgSchema(...).table(...). The Drizzle docs cover both: custom schemas and the relational query API.

Fix#

  1. Check which drizzle-orm is installed. Only 0.39.1 up to 0.40.1 were affected in the runs above.

    bash
    npm ls drizzle-orm
  2. Upgrade drizzle-orm to 0.41.0 or later. The latest stable line was 0.45.4 when this was tested.

    bash
    npm install drizzle-orm@0.45.4

    Upgrading from 0.38.x crosses 0.39.0, whose type changes have their own reported regression: generic helpers that pass a table or subquery to .from() can fail to compile with TS2345 TableLikeHasEmptySelection. If the build now fails there, that page has the call-site workaround.

  3. If you must stay on 0.39.1 to 0.40.1 for now, rename the key so it differs from the database table name. The table name in .table('user', …) stays the same, so no migration is needed.

    ts
    import { integer, pgSchema, text } from 'drizzle-orm/pg-core';
     
    export const appOne = pgSchema('app_one');
     
    // key "appUser", database table still app_one."user"
    export const appUser = appOne.table('user', {
      id: integer('id').generatedAlwaysAsIdentity().primaryKey(),
      name: text('name'),
    });

    Then query it under the new key: db.query.appUser.findMany(). Every db.query.user call has to change too, which is why the upgrade is the better fix. If you build the schema object by hand rather than with import * as schema, the key in that object is what counts, not the export name.

  4. A narrower stop-gap for one hot path: the core query builder kept the schema on every version tested, so db.select().from(user) works on the broken versions as it is. It does not give you the nested with results of db.query.

Verify the fix#

Print the SQL before running it. .toSQL() works on a relational query as it does on a select:

ts
const q = db.query.user.findFirst({
  where: (t, { eq }) => eq(t.id, 1),
  columns: { name: true },
});
console.log(q.toSQL().sql);
console.log(await q);

On 0.40.1, the reproduction printed a FROM without the schema, and the query failed:

text
--- drizzle-orm 0.40.1
findFirst sql: select "name" from "user" where "user"."id" = $1 limit $2
DatabaseError: relation "user" does not exist

On 0.41.0, the same code printed the schema-qualified name with the alias, and returned the row:

text
--- drizzle-orm 0.41.0
findFirst sql: select "name" from "app_one"."user" "user" where "user"."id" = $1 limit $2
result: {"name":"Ada"}

The "user"."id" in the WHERE clause is the alias, not the table, so it stays unqualified on both versions. What matters is the FROM clause: once it reads "app_one"."user" "user", the fix is in.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

When the error persists after upgrading#

If the SQL from .toSQL() now shows the schema but PostgreSQL still says the relation does not exist, the bug above is no longer the cause. Check the physical table: \dt app_one.* in psql lists what is really in the schema, and a table created with mixed case needs the exact quoted spelling. The Prisma 42P01 page walks through that case for raw SQL, where PostgreSQL folds an unquoted name to lowercase; the catalogue checks apply to Drizzle as well.

A deep with tree is the next place where Drizzle's relational aliases meet a PostgreSQL limit. Relational Queries v1 build nested aliases by joining relation keys, and past 63 bytes PostgreSQL truncates them; the symptom is table name "..." specified more than once, a different error with a different fix.

Teams that pinned drizzle-orm 0.38.4 to avoid this bug can move straight to the current 0.45 line: in the runs above, both kept the schema in relational queries, and only the versions between 0.39.1 and 0.40.1 dropped it. Check the SQL with .toSQL() once after the upgrade, and the relation does not exist error from a pgSchema table should not come back for this reason.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote