diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 16c37a233..c90f1f4d0 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -252,12 +252,17 @@ export abstract class BaseCrudDialect { const [field, order] = orderByItems[j]!; const _order = negateOrderBy ? (order === 'asc' ? 'desc' : 'asc') : order; const op = j === i ? (_order === 'asc' ? '>=' : '<=') : '='; + // Fields inherited from a delegate base live on the base table, which is + // joined with its model name as alias. See buildSelectModel/buildDelegateJoin. + const fieldDef = requireField(this.schema, model, field); + const outerAlias = fieldDef.originModel ?? modelAlias; + const subSelectAlias = fieldDef.originModel ?? subQueryAlias; andFilters.push( this.eb( - this.eb.ref(`${modelAlias}.${field}`), + this.eb.ref(`${outerAlias}.${field}`), op, this.buildSelectModel(model, subQueryAlias) - .select(`${subQueryAlias}.${field}`) + .select(`${subSelectAlias}.${field}`) .where(cursorFilter), ), ); diff --git a/tests/regression/test/issue-2588.test.ts b/tests/regression/test/issue-2588.test.ts new file mode 100644 index 000000000..92d92a279 --- /dev/null +++ b/tests/regression/test/issue-2588.test.ts @@ -0,0 +1,78 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// https://github.com/zenstackhq/zenstack/issues/2588 +describe('Regression for issue 2588', () => { + const schema = ` +model Asset { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + assetType String + @@delegate(assetType) +} + +model Notification extends Asset { + title String +} +`; + + async function setup() { + const db = await createTestClient(schema); + const a = await db.notification.create({ + data: { title: 'A', createdAt: new Date('2025-01-01T00:00:00Z') }, + }); + const b = await db.notification.create({ + data: { title: 'B', createdAt: new Date('2025-01-02T00:00:00Z') }, + }); + const c = await db.notification.create({ + data: { title: 'C', createdAt: new Date('2025-01-03T00:00:00Z') }, + }); + return { db, a, b, c }; + } + + it('cursor + orderBy on delegate parent field does not error', async () => { + const { db, b } = await setup(); + + const result = await db.notification.findMany({ + cursor: { id: b.id }, + orderBy: { createdAt: 'asc' }, + }); + + expect(result.map((n: any) => n.title)).toEqual(['B', 'C']); + }); + + it('cursor + skip + orderBy on delegate parent field works', async () => { + const { db, a } = await setup(); + + const result = await db.notification.findMany({ + cursor: { id: a.id }, + skip: 1, + orderBy: { createdAt: 'asc' }, + take: 25, + }); + + expect(result.map((n: any) => n.title)).toEqual(['B', 'C']); + }); + + it('cursor + multiple orderBy mixing child and delegate fields', async () => { + const { db, a } = await setup(); + + const result = await db.notification.findMany({ + cursor: { id: a.id }, + orderBy: [{ createdAt: 'asc' }, { id: 'asc' }], + }); + + expect(result.map((n: any) => n.title)).toEqual(['A', 'B', 'C']); + }); + + it('cursor + orderBy on child field still works', async () => { + const { db, b } = await setup(); + + const result = await db.notification.findMany({ + cursor: { id: b.id }, + orderBy: { title: 'asc' }, + }); + + expect(result.map((n: any) => n.title)).toEqual(['B', 'C']); + }); +});