From f4484132b189f4d3c62486b79f3b81e2f21aaf1e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:41:49 -0700 Subject: [PATCH 1/3] fix(orm): fix _count returning 0 for self-referential relations on delegate models When buildCountJson builds a correlated subquery for _count, it was using fieldModel as the subquery alias. For self-referential relations on delegate models (where fieldModel === model), both sides of the correlated WHERE clause resolved to the inner table, so no rows matched and count was always 0. Fix by generating a unique subQueryAlias via tmpAlias() for the subquery, matching the pattern already used by buildToOneRelationFilter and buildRelationJoinFilter. Fixes #2452 Co-Authored-By: Claude Sonnet 4.6 --- .../src/client/crud/dialects/base-dialect.ts | 12 +++-- tests/regression/test/issue-2452.test.ts | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 tests/regression/test/issue-2452.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 2ae0bb7af..1c7627547 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -1292,25 +1292,29 @@ export abstract class BaseCrudDialect { const fieldModel = fieldDef.type as GetModels; let fieldCountQuery: SelectQueryBuilder; + // Use a unique alias for the subquery to avoid ambiguous references when + // fieldModel === model (self-referential relation on a delegate model) + const subQueryAlias = tmpAlias(`${parentAlias}$_${field}$count`); + // join conditions const m2m = getManyToManyRelation(this.schema, model, field); if (m2m) { // many-to-many relation, count the join table - fieldCountQuery = this.buildModelSelect(fieldModel, fieldModel, value as any, false) + fieldCountQuery = this.buildModelSelect(fieldModel, subQueryAlias, value as any, false) .innerJoin(m2m.joinTable, (join) => join - .onRef(`${m2m.joinTable}.${m2m.otherFkName}`, '=', `${fieldModel}.${m2m.otherPKName}`) + .onRef(`${m2m.joinTable}.${m2m.otherFkName}`, '=', `${subQueryAlias}.${m2m.otherPKName}`) .onRef(`${m2m.joinTable}.${m2m.parentFkName}`, '=', `${parentAlias}.${m2m.parentPKName}`), ) .select(eb.fn.countAll().as(`_count$${field}`)); } else { // build a nested query to count the number of records in the relation - fieldCountQuery = this.buildModelSelect(fieldModel, fieldModel, value as any, false).select( + fieldCountQuery = this.buildModelSelect(fieldModel, subQueryAlias, value as any, false).select( eb.fn.countAll().as(`_count$${field}`), ); // join conditions - const joinPairs = buildJoinPairs(this.schema, model, parentAlias, field, fieldModel); + const joinPairs = buildJoinPairs(this.schema, model, parentAlias, field, subQueryAlias); for (const [left, right] of joinPairs) { fieldCountQuery = fieldCountQuery.whereRef(left, '=', right); } diff --git a/tests/regression/test/issue-2452.test.ts b/tests/regression/test/issue-2452.test.ts new file mode 100644 index 000000000..75499e846 --- /dev/null +++ b/tests/regression/test/issue-2452.test.ts @@ -0,0 +1,47 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// https://github.com/zenstackhq/zenstack/issues/2452 +describe('Regression for issue 2452', () => { + it('should return correct _count for self-referential relations in delegate models', async () => { + const db = await createTestClient( + ` +enum ContentType { + POST + ARTICLE + QUESTION +} + +model Content { + id Int @id @default(autoincrement()) + type ContentType + @@delegate(type) +} + +model Post extends Content { + replies Post[] @relation("PostReplies") + parentId Int? + parent Post? @relation("PostReplies", fields: [parentId], references: [id]) +} + `, + ); + + // Create a parent post with 2 replies + const parent = await db.post.create({ + data: { + replies: { + create: [{}, {}], + }, + }, + }); + + // Query with _count should return the correct count + const result = await db.post.findFirst({ + where: { id: parent.id }, + include: { _count: { select: { replies: true } } }, + }); + + expect(result).toBeTruthy(); + expect(result._count.replies).toBe(2); + }); +}); From 5e185c7c623ac40707e8a83ce1e7ba625ce262a4 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:21:56 -0700 Subject: [PATCH 2/3] test(regression): fix enum case in issue-2452 test for Postgres compatibility Postgres enums are case-sensitive; enum values must match the model name used as the delegate discriminator (e.g. 'Post' not 'POST'). Co-Authored-By: Claude Sonnet 4.6 --- tests/regression/test/issue-2452.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/regression/test/issue-2452.test.ts b/tests/regression/test/issue-2452.test.ts index 75499e846..ea4e241ad 100644 --- a/tests/regression/test/issue-2452.test.ts +++ b/tests/regression/test/issue-2452.test.ts @@ -7,9 +7,9 @@ describe('Regression for issue 2452', () => { const db = await createTestClient( ` enum ContentType { - POST - ARTICLE - QUESTION + Post + Article + Question } model Content { @@ -24,6 +24,7 @@ model Post extends Content { parent Post? @relation("PostReplies", fields: [parentId], references: [id]) } `, + { provider: 'postgresql' }, ); // Create a parent post with 2 replies From 3a475ce6208287577a45690da09064ae02478383 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:23:29 -0700 Subject: [PATCH 3/3] update test --- tests/regression/test/issue-2452.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/regression/test/issue-2452.test.ts b/tests/regression/test/issue-2452.test.ts index ea4e241ad..01586c920 100644 --- a/tests/regression/test/issue-2452.test.ts +++ b/tests/regression/test/issue-2452.test.ts @@ -24,7 +24,6 @@ model Post extends Content { parent Post? @relation("PostReplies", fields: [parentId], references: [id]) } `, - { provider: 'postgresql' }, ); // Create a parent post with 2 replies