From 9cf5d6cad5828f14ae865041b472dc36c4c5cc09 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 9 Jun 2026 14:43:12 +0000 Subject: [PATCH 1/7] fix: `isEmpty` for postgres --- packages/orm/src/client/crud/dialects/postgresql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 41d9b9006..25345ada0 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -409,7 +409,7 @@ export class PostgresCrudDialect extends LateralJoinDi } override buildArrayLength(array: Expression): AliasableExpression { - return this.eb.fn('array_length', [array]); + return this.eb.fn('array_length', [array, sql.lit(1)]); } override buildArrayValue(values: Expression[], elemType: string): AliasableExpression { From 39de44aaafa960809518e12f658c53b105614d2f Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 9 Jun 2026 14:43:47 +0000 Subject: [PATCH 2/7] fix: `in` expressions with enums --- packages/orm/src/client/crud/dialects/postgresql.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 25345ada0..8de870b88 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -414,6 +414,9 @@ export class PostgresCrudDialect extends LateralJoinDi override buildArrayValue(values: Expression[], elemType: string): AliasableExpression { const arr = sql`ARRAY[${sql.join(values, sql.raw(','))}]`; + if (isEnum(this.schema, elemType)) { + return this.eb.cast(arr, sql`${sql.id(elemType)}[]`); + } const mappedType = this.getSqlType(elemType); return this.eb.cast(arr, sql`${sql.raw(mappedType)}[]`); } From a2f1134aad9db8f9bb842fcf31aec95c131429b0 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 9 Jun 2026 14:44:09 +0000 Subject: [PATCH 3/7] chore: add tests --- tests/e2e/orm/client-api/pg-dialect.test.ts | 72 +++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/e2e/orm/client-api/pg-dialect.test.ts diff --git a/tests/e2e/orm/client-api/pg-dialect.test.ts b/tests/e2e/orm/client-api/pg-dialect.test.ts new file mode 100644 index 000000000..5b3726ba0 --- /dev/null +++ b/tests/e2e/orm/client-api/pg-dialect.test.ts @@ -0,0 +1,72 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Postgres dialect tests', () => { + it('isEmpty function', async () => { + const schema = ` +model User { + id Int @id @default(autoincrement()) + roles Role[] + + @@allow('all', true) + @@deny('create', isEmpty(roles)) +} + +enum Role { + AUTHOR + EDITOR +} + `; + + const client = await createPolicyTestClient(schema, { + usePrismaPush: true, + provider: 'postgresql', + }); + + await expect(client.user.create({ + data: { + id: 1, + roles: ['AUTHOR'], + }, + })).resolves.toMatchObject({ + id: 1, + roles: ['AUTHOR'], + }); + }); + + it('in expression with enums', async () => { + const schema = ` +model User { + id Int @id @default(autoincrement()) + role Role + + @@allow('all', true) + @@deny('delete', role in [EDITOR]) +} + +enum Role { + AUTHOR + EDITOR +} + `; + + const client = await createPolicyTestClient(schema, { + usePrismaPush: true, + provider: 'postgresql', + }); + + await expect(client.user.create({ + data: { + id: 1, + role: 'AUTHOR', + }, + })).resolves.toMatchObject({ + id: 1, + role: 'AUTHOR', + }); + + await expect(client.user.deleteMany()).resolves.toMatchObject({ + count: 1, + }); + }); +}); From 88a75a090a35a2d0c63fdcb47acf6dccb66b75e7 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 9 Jun 2026 15:37:37 +0000 Subject: [PATCH 4/7] experimenting --- .../src/client/crud/dialects/base-dialect.ts | 2 +- .../orm/src/client/crud/dialects/postgresql.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 4068f5ccf..9df48955d 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -549,7 +549,7 @@ export abstract class BaseCrudDialect { let receiver = fieldRef; if (isEnum(this.schema, fieldType)) { // cast enum array to `text[]` for type compatibility - receiver = this.eb.cast(fieldRef, sql.raw('text[]')); + receiver = this.eb.cast(fieldRef, sql`${sql.id(fieldType)}[]`); } const buildArray = (value: unknown) => { diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 8de870b88..d33e19560 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -414,11 +414,11 @@ export class PostgresCrudDialect extends LateralJoinDi override buildArrayValue(values: Expression[], elemType: string): AliasableExpression { const arr = sql`ARRAY[${sql.join(values, sql.raw(','))}]`; - if (isEnum(this.schema, elemType)) { - return this.eb.cast(arr, sql`${sql.id(elemType)}[]`); - } + // if (isEnum(this.schema, elemType)) { + // return this.eb.cast(arr, sql`${sql.id(elemType)}[]`); + // } const mappedType = this.getSqlType(elemType); - return this.eb.cast(arr, sql`${sql.raw(mappedType)}[]`); + return this.eb.cast(arr, sql`${sql.id(mappedType)}[]`); } override buildArrayContains( @@ -430,7 +430,7 @@ export class PostgresCrudDialect extends LateralJoinDi const arrayExpr = sql`ARRAY[${value}]`; if (elemType) { const mappedType = this.getSqlType(elemType); - const typedArray = this.eb.cast(arrayExpr, sql`${sql.raw(mappedType)}[]`); + const typedArray = this.eb.cast(arrayExpr, sql`${sql.id(mappedType)}[]`); return this.eb(field, '@>', typedArray); } else { return this.eb(field, '@>', arrayExpr); @@ -504,7 +504,7 @@ export class PostgresCrudDialect extends LateralJoinDi } if (isEnum(this.schema, zmodelType)) { // reduce enum to text for type compatibility - return 'text'; + return zmodelType; } else { return this.zmodelToSqlTypeMap[zmodelType] ?? 'text'; } @@ -543,10 +543,10 @@ export class PostgresCrudDialect extends LateralJoinDi (leftResolved.hasDbOverride || rightResolved.hasDbOverride) ) { if (leftResolved.hasDbOverride) { - left = this.eb.cast(left, sql.raw(this.getSqlType(leftFieldDef!.type))); + left = this.eb.cast(left, sql.id(this.getSqlType(leftFieldDef!.type))); } if (rightResolved.hasDbOverride) { - right = this.eb.cast(right, sql.raw(this.getSqlType(rightFieldDef!.type))); + right = this.eb.cast(right, sql.id(this.getSqlType(rightFieldDef!.type))); } } return super.buildComparison(left, leftFieldDef, op, right, rightFieldDef); @@ -587,7 +587,7 @@ export class PostgresCrudDialect extends LateralJoinDi .select( fields.map((f, i) => { const mappedType = this.getSqlType(f.type, f.attributes); - const castType = f.array ? sql`${sql.raw(mappedType)}[]` : sql.raw(mappedType); + const castType = f.array ? sql`${sql.id(mappedType)}[]` : sql.id(mappedType); return this.eb.cast(sql.ref(`$values.column${i + 1}`), castType).as(f.name); }), ); From b071d39d191fcd70a3fbc7fdfcad6cabad2ce6ae Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 9 Jun 2026 16:16:42 +0000 Subject: [PATCH 5/7] Revert "experimenting" --- .../src/client/crud/dialects/base-dialect.ts | 2 +- .../orm/src/client/crud/dialects/postgresql.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 9df48955d..4068f5ccf 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -549,7 +549,7 @@ export abstract class BaseCrudDialect { let receiver = fieldRef; if (isEnum(this.schema, fieldType)) { // cast enum array to `text[]` for type compatibility - receiver = this.eb.cast(fieldRef, sql`${sql.id(fieldType)}[]`); + receiver = this.eb.cast(fieldRef, sql.raw('text[]')); } const buildArray = (value: unknown) => { diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index d33e19560..8de870b88 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -414,11 +414,11 @@ export class PostgresCrudDialect extends LateralJoinDi override buildArrayValue(values: Expression[], elemType: string): AliasableExpression { const arr = sql`ARRAY[${sql.join(values, sql.raw(','))}]`; - // if (isEnum(this.schema, elemType)) { - // return this.eb.cast(arr, sql`${sql.id(elemType)}[]`); - // } + if (isEnum(this.schema, elemType)) { + return this.eb.cast(arr, sql`${sql.id(elemType)}[]`); + } const mappedType = this.getSqlType(elemType); - return this.eb.cast(arr, sql`${sql.id(mappedType)}[]`); + return this.eb.cast(arr, sql`${sql.raw(mappedType)}[]`); } override buildArrayContains( @@ -430,7 +430,7 @@ export class PostgresCrudDialect extends LateralJoinDi const arrayExpr = sql`ARRAY[${value}]`; if (elemType) { const mappedType = this.getSqlType(elemType); - const typedArray = this.eb.cast(arrayExpr, sql`${sql.id(mappedType)}[]`); + const typedArray = this.eb.cast(arrayExpr, sql`${sql.raw(mappedType)}[]`); return this.eb(field, '@>', typedArray); } else { return this.eb(field, '@>', arrayExpr); @@ -504,7 +504,7 @@ export class PostgresCrudDialect extends LateralJoinDi } if (isEnum(this.schema, zmodelType)) { // reduce enum to text for type compatibility - return zmodelType; + return 'text'; } else { return this.zmodelToSqlTypeMap[zmodelType] ?? 'text'; } @@ -543,10 +543,10 @@ export class PostgresCrudDialect extends LateralJoinDi (leftResolved.hasDbOverride || rightResolved.hasDbOverride) ) { if (leftResolved.hasDbOverride) { - left = this.eb.cast(left, sql.id(this.getSqlType(leftFieldDef!.type))); + left = this.eb.cast(left, sql.raw(this.getSqlType(leftFieldDef!.type))); } if (rightResolved.hasDbOverride) { - right = this.eb.cast(right, sql.id(this.getSqlType(rightFieldDef!.type))); + right = this.eb.cast(right, sql.raw(this.getSqlType(rightFieldDef!.type))); } } return super.buildComparison(left, leftFieldDef, op, right, rightFieldDef); @@ -587,7 +587,7 @@ export class PostgresCrudDialect extends LateralJoinDi .select( fields.map((f, i) => { const mappedType = this.getSqlType(f.type, f.attributes); - const castType = f.array ? sql`${sql.id(mappedType)}[]` : sql.id(mappedType); + const castType = f.array ? sql`${sql.raw(mappedType)}[]` : sql.raw(mappedType); return this.eb.cast(sql.ref(`$values.column${i + 1}`), castType).as(f.name); }), ); From f1ee8c274a1b6d63453b5553f6f87a90a415fc05 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 9 Jun 2026 16:17:22 +0000 Subject: [PATCH 6/7] remove `in` expression fix --- .../src/client/crud/dialects/postgresql.ts | 3 -- tests/e2e/orm/client-api/pg-dialect.test.ts | 36 ------------------- 2 files changed, 39 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 8de870b88..25345ada0 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -414,9 +414,6 @@ export class PostgresCrudDialect extends LateralJoinDi override buildArrayValue(values: Expression[], elemType: string): AliasableExpression { const arr = sql`ARRAY[${sql.join(values, sql.raw(','))}]`; - if (isEnum(this.schema, elemType)) { - return this.eb.cast(arr, sql`${sql.id(elemType)}[]`); - } const mappedType = this.getSqlType(elemType); return this.eb.cast(arr, sql`${sql.raw(mappedType)}[]`); } diff --git a/tests/e2e/orm/client-api/pg-dialect.test.ts b/tests/e2e/orm/client-api/pg-dialect.test.ts index 5b3726ba0..14ab8bc4d 100644 --- a/tests/e2e/orm/client-api/pg-dialect.test.ts +++ b/tests/e2e/orm/client-api/pg-dialect.test.ts @@ -33,40 +33,4 @@ enum Role { roles: ['AUTHOR'], }); }); - - it('in expression with enums', async () => { - const schema = ` -model User { - id Int @id @default(autoincrement()) - role Role - - @@allow('all', true) - @@deny('delete', role in [EDITOR]) -} - -enum Role { - AUTHOR - EDITOR -} - `; - - const client = await createPolicyTestClient(schema, { - usePrismaPush: true, - provider: 'postgresql', - }); - - await expect(client.user.create({ - data: { - id: 1, - role: 'AUTHOR', - }, - })).resolves.toMatchObject({ - id: 1, - role: 'AUTHOR', - }); - - await expect(client.user.deleteMany()).resolves.toMatchObject({ - count: 1, - }); - }); }); From 3e5e76ffaf1f132b33601ad071c2ef6ef0af0939 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Tue, 9 Jun 2026 17:08:33 +0000 Subject: [PATCH 7/7] chore: move test --- .../pg-dialect.test.ts => policy/isempty-function-pg.test.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/e2e/orm/{client-api/pg-dialect.test.ts => policy/isempty-function-pg.test.ts} (86%) diff --git a/tests/e2e/orm/client-api/pg-dialect.test.ts b/tests/e2e/orm/policy/isempty-function-pg.test.ts similarity index 86% rename from tests/e2e/orm/client-api/pg-dialect.test.ts rename to tests/e2e/orm/policy/isempty-function-pg.test.ts index 14ab8bc4d..9af038d02 100644 --- a/tests/e2e/orm/client-api/pg-dialect.test.ts +++ b/tests/e2e/orm/policy/isempty-function-pg.test.ts @@ -1,8 +1,8 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -describe('Postgres dialect tests', () => { - it('isEmpty function', async () => { +describe('isEmpty function in policies using Postgres', () => { + it('does not throw an error', async () => { const schema = ` model User { id Int @id @default(autoincrement())