From afc9d4453d18cc8486c2cea5345bfe9baa638094 Mon Sep 17 00:00:00 2001 From: Lukas Kahwe Smith Date: Tue, 31 Mar 2026 20:48:17 +0200 Subject: [PATCH 1/2] include mixin-inherited @computed fields in model's computedFields schema property --- packages/sdk/src/ts-schema-generator.ts | 4 +- .../computed-field-nested-include.test.ts | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/orm/client-api/computed-field-nested-include.test.ts diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index fda31fe97..cc2c5b876 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -453,7 +453,9 @@ export class TsSchemaGenerator { ...(dm.isView ? [ts.factory.createPropertyAssignment('isView', ts.factory.createTrue())] : []), ]; - const computedFields = dm.fields.filter((f) => hasAttribute(f, '@computed')); + const computedFields = allFields.filter( + (f) => hasAttribute(f, '@computed') && !getDelegateOriginModel(f, dm), + ); if (computedFields.length > 0) { fields.push( diff --git a/tests/e2e/orm/client-api/computed-field-nested-include.test.ts b/tests/e2e/orm/client-api/computed-field-nested-include.test.ts new file mode 100644 index 000000000..531eb8d76 --- /dev/null +++ b/tests/e2e/orm/client-api/computed-field-nested-include.test.ts @@ -0,0 +1,87 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// Bug: when a model with @computed fields inherited from a mixin type (the `with` +// keyword) is fetched as a nested relation via an explicit `include`, ZenStack +// emits the computed field name as a raw column reference inside `jsonb_build_object` +// while the inner subquery SELECT only contains the real DB columns. This causes +// PostgreSQL to fail with: +// "column $$tN.field_name does not exist" +// +// Root cause: `buildSelectField` uses `fieldDef.originModel` (the mixin type name) +// as the table alias when selecting the computed field into the inner subquery. +// But the inner subquery aliases the actual table under the model name, not the +// mixin type name, so the correlated subquery is never emitted and `parentCode` +// is absent from the subquery's SELECT list. The outer `jsonb_build_object` then +// references `$$tN.parentCode` which does not exist. +// +// The bug does NOT occur when: +// - the @computed field is declared directly on the model (not via a mixin type) +// - the relation is not explicitly included +// - the model is queried directly (not as a nested include) + +describe('Computed fields with nested include', () => { + it('includes computed fields inherited from a mixin type when the model is explicitly included', async () => { + const db = await createTestClient( + ` +type ParentRelated { + parentCode String? @computed +} + +model Parent { + id Int @id @default(autoincrement()) + code String + children Child[] +} + +model Child with ParentRelated { + id Int @id @default(autoincrement()) + name String + parentId Int + parent Parent @relation(fields: [parentId], references: [id]) +} + `, + { + provider: 'postgresql', + computedFields: { + Child: { + // Correlated subquery — looks up Parent.code via the FK. + // The computed field is inherited from the `ParentRelated` mixin, + // so fieldDef.originModel === 'ParentRelated', which is the + // alias incorrectly used by buildSelectField. + parentCode: (eb: any) => + eb + .selectFrom('Parent') + .select('Parent.code') + .whereRef('Parent.id', '=', 'parentId') + .limit(1), + }, + }, + } as any, + ); + + const parent = await db.parent.create({ + data: { code: 'P-001', children: { create: [{ name: 'Alice' }, { name: 'Bob' }] } }, + }); + + // Direct query on Child works fine + await expect(db.child.findFirst({ where: { parentId: parent.id } })).resolves.toMatchObject({ + parentCode: 'P-001', + }); + + // Querying Parent with include: { children: true } should also work, + // but currently fails with "column $$tN.parentCode does not exist" + await expect( + db.parent.findFirst({ + where: { id: parent.id }, + include: { children: true }, + }), + ).resolves.toMatchObject({ + code: 'P-001', + children: expect.arrayContaining([ + expect.objectContaining({ name: 'Alice', parentCode: 'P-001' }), + expect.objectContaining({ name: 'Bob', parentCode: 'P-001' }), + ]), + }); + }); +}); From 30385b6bb5f121940c2a7625093e0ff9c90e797e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:11:16 -0700 Subject: [PATCH 2/2] test(regression): add regression test for issue #2540 Move computed-field nested include test from e2e to regression suite as issue-2540. Co-Authored-By: Claude Sonnet 4.6 --- .../test/issue-2540.test.ts} | 28 ++----------------- 1 file changed, 2 insertions(+), 26 deletions(-) rename tests/{e2e/orm/client-api/computed-field-nested-include.test.ts => regression/test/issue-2540.test.ts} (56%) diff --git a/tests/e2e/orm/client-api/computed-field-nested-include.test.ts b/tests/regression/test/issue-2540.test.ts similarity index 56% rename from tests/e2e/orm/client-api/computed-field-nested-include.test.ts rename to tests/regression/test/issue-2540.test.ts index 531eb8d76..e193ebfd7 100644 --- a/tests/e2e/orm/client-api/computed-field-nested-include.test.ts +++ b/tests/regression/test/issue-2540.test.ts @@ -1,26 +1,7 @@ import { createTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -// Bug: when a model with @computed fields inherited from a mixin type (the `with` -// keyword) is fetched as a nested relation via an explicit `include`, ZenStack -// emits the computed field name as a raw column reference inside `jsonb_build_object` -// while the inner subquery SELECT only contains the real DB columns. This causes -// PostgreSQL to fail with: -// "column $$tN.field_name does not exist" -// -// Root cause: `buildSelectField` uses `fieldDef.originModel` (the mixin type name) -// as the table alias when selecting the computed field into the inner subquery. -// But the inner subquery aliases the actual table under the model name, not the -// mixin type name, so the correlated subquery is never emitted and `parentCode` -// is absent from the subquery's SELECT list. The outer `jsonb_build_object` then -// references `$$tN.parentCode` which does not exist. -// -// The bug does NOT occur when: -// - the @computed field is declared directly on the model (not via a mixin type) -// - the relation is not explicitly included -// - the model is queried directly (not as a nested include) - -describe('Computed fields with nested include', () => { +describe('Regression for issue #2540', () => { it('includes computed fields inherited from a mixin type when the model is explicitly included', async () => { const db = await createTestClient( ` @@ -45,10 +26,6 @@ model Child with ParentRelated { provider: 'postgresql', computedFields: { Child: { - // Correlated subquery — looks up Parent.code via the FK. - // The computed field is inherited from the `ParentRelated` mixin, - // so fieldDef.originModel === 'ParentRelated', which is the - // alias incorrectly used by buildSelectField. parentCode: (eb: any) => eb .selectFrom('Parent') @@ -69,8 +46,7 @@ model Child with ParentRelated { parentCode: 'P-001', }); - // Querying Parent with include: { children: true } should also work, - // but currently fails with "column $$tN.parentCode does not exist" + // Querying Parent with include: { children: true } should also work await expect( db.parent.findFirst({ where: { id: parent.id },