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 })]); + }); +});