From 3e3e6476e76ae7e935db26d317d27513d972ec7f Mon Sep 17 00:00:00 2001
From: ymc9 <104139426+ymc9@users.noreply.github.com>
Date: Mon, 27 Jul 2026 23:44:04 +0800
Subject: [PATCH] fix(orm): to-one relation filter missing delegate base join
Filtering a to-one relation whose target is a polymorphic subtype by a
field inherited from the delegate base failed with `missing FROM-clause
entry for table ""` (Postgres) / `no such column: .`
(SQLite).
`buildToOneRelationFilter` built its `exists` subquery with a bare
`selectFrom` on the concrete model, so the delegate base tables were
never joined. `buildFilter` references base-inherited fields through
`fieldDef.originModel`, i.e. qualified with the base model name, so the
generated SQL named a table absent from the FROM clause.
Use `buildSelectModel` - the same helper `buildToManyRelationFilter`
already uses, which joins all delegate bases. That's why `some`/`none`
filters worked while the to-one form didn't.
Fixes #2768
Co-Authored-By: Claude Opus 5 (1M context)
---
.../src/client/crud/dialects/base-dialect.ts | 5 +-
tests/regression/test/issue-2768.test.ts | 94 +++++++++++++++++++
2 files changed, 97 insertions(+), 2 deletions(-)
create mode 100644 tests/regression/test/issue-2768.test.ts
diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts
index 180405f31..f126cfeef 100644
--- a/packages/orm/src/client/crud/dialects/base-dialect.ts
+++ b/packages/orm/src/client/crud/dialects/base-dialect.ts
@@ -387,8 +387,9 @@ export abstract class BaseCrudDialect {
field,
joinAlias,
);
- const baseJoin = this.eb
- .selectFrom(`${fieldDef.type} as ${joinAlias}`)
+ // use `buildSelectModel` so that delegate base tables are joined - the filter may
+ // reference fields inherited from a base, which live on the base table
+ const baseJoin = this.buildSelectModel(fieldDef.type, joinAlias)
.select(this.eb.lit(1).as('_'))
.where(() =>
this.and(...joinPairs.map(([left, right]) => this.eb(this.eb.ref(left), '=', this.eb.ref(right)))),
diff --git a/tests/regression/test/issue-2768.test.ts b/tests/regression/test/issue-2768.test.ts
new file mode 100644
index 000000000..8a5b7a3a1
--- /dev/null
+++ b/tests/regression/test/issue-2768.test.ts
@@ -0,0 +1,94 @@
+import { createTestClient } from '@zenstackhq/testtools';
+import { describe, expect, it } from 'vitest';
+
+const schema = `
+model Location {
+ id String @id @default(cuid())
+ name String
+ resources Resource[]
+}
+
+enum ResourceType {
+ DESK
+}
+
+model Resource {
+ id String @id @default(cuid())
+ name String
+ type ResourceType
+ locationId String
+ location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
+
+ @@delegate(type)
+}
+
+model ResourceDesk extends Resource {
+ employeeDeskSessions EmployeeDeskSession[]
+
+ @@delegateMap(DESK)
+}
+
+model EmployeeDeskSession {
+ id String @id @default(cuid())
+ deskId String
+ desk ResourceDesk @relation(fields: [deskId], references: [id], onDelete: Cascade)
+}
+`;
+
+describe('Regression for issue 2768', () => {
+ async function createData() {
+ const db = await createTestClient(schema);
+
+ const l1 = await db.location.create({ data: { id: 'l1', name: 'l1' } });
+ const l2 = await db.location.create({ data: { id: 'l2', name: 'l2' } });
+
+ const desk1 = await db.resourceDesk.create({ data: { name: 'desk1', locationId: l1.id } });
+ const desk2 = await db.resourceDesk.create({ data: { name: 'desk2', locationId: l2.id } });
+
+ await db.employeeDeskSession.create({ data: { id: 's1', deskId: desk1.id } });
+ await db.employeeDeskSession.create({ data: { id: 's2', deskId: desk2.id } });
+
+ return db;
+ }
+
+ it('filters a to-one relation by a base-model foreign key field', async () => {
+ const db = await createData();
+ await expect(db.employeeDeskSession.findMany({ where: { desk: { locationId: 'l1' } } })).resolves.toEqual([
+ expect.objectContaining({ id: 's1' }),
+ ]);
+ });
+
+ it('filters a to-one relation by a base-model scalar field', async () => {
+ const db = await createData();
+ await expect(
+ db.employeeDeskSession.findMany({ where: { desk: { name: { startsWith: 'desk1' } } } }),
+ ).resolves.toEqual([expect.objectContaining({ id: 's1' })]);
+ });
+
+ it('filters a to-one relation by a base-model relation', async () => {
+ const db = await createData();
+ await expect(
+ db.employeeDeskSession.findMany({ where: { desk: { location: { name: 'l2' } } } }),
+ ).resolves.toEqual([expect.objectContaining({ id: 's2' })]);
+ });
+
+ it('filters a to-many relation by a field inherited from the delegate base', async () => {
+ const db = await createTestClient(schema);
+
+ const l1 = await db.location.create({ data: { name: 'l1' } });
+ const l2 = await db.location.create({ data: { name: 'l2' } });
+
+ const desk1 = await db.resourceDesk.create({ data: { name: 'desk1', locationId: l1.id } });
+ await db.resourceDesk.create({ data: { name: 'desk2', locationId: l2.id } });
+
+ await db.employeeDeskSession.create({ data: { id: 's1', deskId: desk1.id } });
+
+ await expect(
+ db.location.findMany({ where: { resources: { some: { name: 'desk1' } } } }),
+ ).resolves.toEqual([expect.objectContaining({ id: l1.id })]);
+
+ await expect(
+ db.location.findMany({ where: { resources: { none: { name: 'desk1' } } } }),
+ ).resolves.toEqual([expect.objectContaining({ id: l2.id })]);
+ });
+});