From 10ace8c984469ec8b864ab51e431b931d5f27736 Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:41:28 -0700 Subject: [PATCH 1/4] fix(orm): allow setting `null` on optional `Decimal` fields when using `sqlite` provider (#2798) --- .../orm/src/client/crud/dialects/sqlite.ts | 2 +- tests/e2e/orm/client-api/sqlite-null.test.ts | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/orm/client-api/sqlite-null.test.ts diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index 00927f510..a7ceac7b2 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -99,7 +99,7 @@ export class SqliteCrudDialect extends BaseCrudDialect ? new Date(value).toISOString() : value; case 'Decimal': - return (value as Decimal).toString(); + return value !== null ? value.toString() : value; case 'Bytes': return Buffer.from(value as Uint8Array); default: diff --git a/tests/e2e/orm/client-api/sqlite-null.test.ts b/tests/e2e/orm/client-api/sqlite-null.test.ts new file mode 100644 index 000000000..912788d04 --- /dev/null +++ b/tests/e2e/orm/client-api/sqlite-null.test.ts @@ -0,0 +1,52 @@ +import { createTestClient, getTestDbProvider } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; +import Decimal from 'decimal.js'; + +const provider = getTestDbProvider(); + +describe.skipIf(provider !== 'sqlite')('sqlite null tests', () => { + it('allows writing null to optional Decimal fields', async () => { + const db = await createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + balance Decimal? +} +`, + ); + + const user = await db.user.create({ + data: { + balance: null, + }, + }); + + expect(user).toMatchObject({ + balance: null, + }); + + await db.user.update({ + data: { + balance: Decimal(1), + }, + + where: { + id: user.id, + }, + }); + + await expect( + db.user.update({ + data: { + balance: null, + }, + + where: { + id: user.id, + }, + }), + ).resolves.toMatchObject({ + balance: null, + }); + }); +}); From 0ac5be5538c6389ca3a508ace3fceb562bbe76a4 Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:06:00 -0700 Subject: [PATCH 2/4] fix(language): add missing `plugin` validation (#2806) --- packages/language/src/utils.ts | 2 +- packages/language/src/validator.ts | 7 ++ .../src/validators/plugin-validator.ts | 31 +++++ packages/language/test/plugin.test.ts | 119 ++++++++++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 packages/language/src/validators/plugin-validator.ts create mode 100644 packages/language/test/plugin.test.ts diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index f733cb0d4..6c184babd 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -745,7 +745,7 @@ export function getPluginDocuments(model: Model, schemaPath: string): string[] { } const provider = getLiteral(providerField.value); - if (!provider) { + if (!provider || typeof provider !== 'string') { continue; } diff --git a/packages/language/src/validator.ts b/packages/language/src/validator.ts index 03a37fe70..dff82fbbb 100644 --- a/packages/language/src/validator.ts +++ b/packages/language/src/validator.ts @@ -9,6 +9,7 @@ import type { GeneratorDecl, InvocationExpr, Model, + Plugin, Procedure, TypeDef, ZModelAstType, @@ -17,6 +18,7 @@ import type { ZModelServices } from './module'; import AttributeValidator from './validators/attribute-validator'; import DataModelValidator from './validators/datamodel-validator'; import DataSourceValidator from './validators/datasource-validator'; +import PluginValidator from './validators/plugin-validator'; import EnumValidator from './validators/enum-validator'; import ExpressionValidator from './validators/expression-validator'; import FunctionDeclValidator from './validators/function-decl-validator'; @@ -34,6 +36,7 @@ export function registerValidationChecks(services: ZModelServices) { const checks: ValidationChecks = { Model: validator.checkModel, DataSource: validator.checkDataSource, + Plugin: validator.checkPlugin, GeneratorDecl: validator.checkGenerator, DataModel: validator.checkDataModel, TypeDef: validator.checkTypeDef, @@ -96,4 +99,8 @@ export class ZModelValidator { checkProcedure(node: Procedure, accept: ValidationAcceptor): void { new ProcedureValidator().validate(node, accept); } + + checkPlugin(node: Plugin, accept: ValidationAcceptor): void { + new PluginValidator().validate(node, accept); + } } diff --git a/packages/language/src/validators/plugin-validator.ts b/packages/language/src/validators/plugin-validator.ts new file mode 100644 index 000000000..28b11ddde --- /dev/null +++ b/packages/language/src/validators/plugin-validator.ts @@ -0,0 +1,31 @@ +import type { ValidationAcceptor } from 'langium'; +import { Plugin } from '../generated/ast'; +import { getStringLiteral } from '../utils'; +import { validateDuplicatedDeclarations, type AstValidator } from './common'; + +/** + * Validates plugin declarations. + */ +export default class PluginValidator implements AstValidator { + validate(plugin: Plugin, accept: ValidationAcceptor): void { + validateDuplicatedDeclarations(plugin, plugin.fields, accept); + this.validateProvider(plugin, accept); + } + + private validateProvider(plugin: Plugin, accept: ValidationAcceptor) { + const provider = plugin.fields.find((f) => f.name === 'provider'); + if (!provider) { + accept('error', 'plugin must include a "provider" field', { + node: plugin, + }); + return; + } + + const providerValue = getStringLiteral(provider.value); + if (!providerValue) { + accept('error', '"provider" must be set to a non-empty string literal', { + node: provider.value, + }); + } + } +} diff --git a/packages/language/test/plugin.test.ts b/packages/language/test/plugin.test.ts new file mode 100644 index 000000000..8f7ca1e8c --- /dev/null +++ b/packages/language/test/plugin.test.ts @@ -0,0 +1,119 @@ +import { describe, it } from 'vitest'; +import { loadSchema, loadSchemaWithError } from './utils'; + +describe('Plugin tests', () => { + it('accepts plugins with a string provider', async () => { + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = 'test' + } + `, + ); + }); + + it('rejects plugins without a provider', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + + } + `, + 'plugin must include a "provider" field', + ); + }); + + it('rejects plugins with an empty provider', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = '' + } + `, + '"provider" must be set to a non-empty string literal', + ); + }); + + it('rejects plugins with a non-string provider', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = [] + } + `, + '"provider" must be set to a non-empty string literal', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = true + } + `, + '"provider" must be set to a non-empty string literal', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = {} + } + `, + '"provider" must be set to a non-empty string literal', + ); + }); +}); From 11a69fe0296ff724710d4afdc245a14e82527909 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:47:05 +0800 Subject: [PATCH 3/4] [CI] Bump version 3.9.3 (#2823) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- package.json | 2 +- packages/auth-adapters/better-auth/package.json | 2 +- packages/cli/package.json | 2 +- packages/clients/client-helpers/package.json | 2 +- packages/clients/fetch-client/package.json | 2 +- packages/clients/tanstack-query/package.json | 2 +- packages/common-helpers/package.json | 2 +- packages/config/eslint-config/package.json | 2 +- packages/config/tsdown-config/package.json | 2 +- packages/config/typescript-config/package.json | 2 +- packages/config/vitest-config/package.json | 2 +- packages/create-zenstack/package.json | 2 +- packages/ide/vscode/package.json | 2 +- packages/language/package.json | 2 +- packages/orm/package.json | 2 +- packages/plugins/policy/package.json | 2 +- packages/plugins/soft-delete/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- packages/zod/package.json | 2 +- samples/orm/package.json | 2 +- samples/taskforge/package.json | 2 +- tests/e2e/package.json | 2 +- tests/regression/package.json | 2 +- tests/runtimes/bun/package.json | 2 +- tests/runtimes/edge-runtime/package.json | 2 +- 28 files changed, 28 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index f882ddfc6..1e6c7ae20 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "zenstack-v3", "displayName": "ZenStack", "description": "ZenStack", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json index eca308d13..6f99bea89 100644 --- a/packages/auth-adapters/better-auth/package.json +++ b/packages/auth-adapters/better-auth/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/better-auth", "displayName": "ZenStack Better Auth Adapter", "description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/cli/package.json b/packages/cli/package.json index 040a4cc5b..bafccb1ab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/cli", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/clients/client-helpers/package.json b/packages/clients/client-helpers/package.json index e3d78453d..2aad5c83e 100644 --- a/packages/clients/client-helpers/package.json +++ b/packages/clients/client-helpers/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/client-helpers", "displayName": "ZenStack Client Helpers", "description": "Helpers for implementing clients that consume ZenStack's CRUD service", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/clients/fetch-client/package.json b/packages/clients/fetch-client/package.json index 9f1ced501..8ac360f1e 100644 --- a/packages/clients/fetch-client/package.json +++ b/packages/clients/fetch-client/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/fetch-client", "displayName": "ZenStack Fetch Client", "description": "Simple fetch-based client for consuming ZenStack's RPC-style CRUD API", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index 68335e478..91ffaa8d2 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack TanStack Query Integration", "description": "TanStack Query Client for consuming ZenStack v3's CRUD service", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index 1638a83d3..9511d797a 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/common-helpers", "displayName": "ZenStack Common Helpers", "description": "ZenStack Common Helpers", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json index 05495eba9..90af2ab02 100644 --- a/packages/config/eslint-config/package.json +++ b/packages/config/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "private": true, "license": "MIT" diff --git a/packages/config/tsdown-config/package.json b/packages/config/tsdown-config/package.json index 22f8400e4..f4d1a663c 100644 --- a/packages/config/tsdown-config/package.json +++ b/packages/config/tsdown-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tsdown-config", - "version": "3.9.2", + "version": "3.9.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/config/typescript-config/package.json b/packages/config/typescript-config/package.json index 389552bd3..01fa90127 100644 --- a/packages/config/typescript-config/package.json +++ b/packages/config/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.9.2", + "version": "3.9.3", "private": true, "license": "MIT" } diff --git a/packages/config/vitest-config/package.json b/packages/config/vitest-config/package.json index 2fa25f0fd..29f35e5b3 100644 --- a/packages/config/vitest-config/package.json +++ b/packages/config/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.9.2", + "version": "3.9.3", "private": true, "license": "MIT", "exports": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 3406e1d34..b6a036a6f 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -2,7 +2,7 @@ "name": "create-zenstack", "displayName": "Create ZenStack", "description": "Create a new ZenStack project", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 9b6bf479a..22e9719e7 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack-v3", "publisher": "zenstack", - "version": "3.9.2", + "version": "3.9.3", "displayName": "ZenStack V3 Language Tools", "description": "VSCode extension for ZenStack (v3) ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index f93aaafb9..db8a41558 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/language", "displayName": "ZenStack Language Tooling", "description": "ZenStack ZModel language specification", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/orm/package.json b/packages/orm/package.json index c6a9419d9..daefd9a76 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/orm", "displayName": "ZenStack ORM", "description": "ZenStack ORM", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json index d9f929f7d..bbd07520f 100644 --- a/packages/plugins/policy/package.json +++ b/packages/plugins/policy/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/plugin-policy", "displayName": "ZenStack Access Policy Plugin", "description": "ZenStack plugin that enforces access control policies defined in the schema", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/plugins/soft-delete/package.json b/packages/plugins/soft-delete/package.json index c921bc8af..7bea6c37b 100644 --- a/packages/plugins/soft-delete/package.json +++ b/packages/plugins/soft-delete/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/plugin-soft-delete", "displayName": "ZenStack Soft Delete Plugin", "description": "ZenStack plugin that implements soft-delete by intercepting Kysely queries", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/schema/package.json b/packages/schema/package.json index 672a9917c..6bd9dd7f2 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/schema", "displayName": "ZenStack Schema Object Model", "description": "TypeScript representation of ZModel schema", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index cc25aa3e4..c4925af52 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/sdk", "displayName": "ZenStack SDK", "description": "Utilities for building ZenStack plugins", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/server/package.json b/packages/server/package.json index 5276c6a81..38cafa65c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/server", "displayName": "ZenStack Automatic CRUD Server", "description": "ZenStack automatic CRUD API handlers and server adapters for popular frameworks", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 6e58df121..f74273ed2 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/testtools", "displayName": "ZenStack Test Tools", "description": "ZenStack Test Tools", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/packages/zod/package.json b/packages/zod/package.json index cf316c038..3605da60a 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -2,7 +2,7 @@ "name": "@zenstackhq/zod", "displayName": "ZenStack Zod Integration", "description": "Automatically deriving Zod schemas from ZModel schemas", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "author": { "name": "ZenStack Team", diff --git a/samples/orm/package.json b/samples/orm/package.json index f0ffff09e..7c95a6fdf 100644 --- a/samples/orm/package.json +++ b/samples/orm/package.json @@ -1,6 +1,6 @@ { "name": "sample-orm", - "version": "3.9.2", + "version": "3.9.3", "description": "", "main": "index.js", "private": true, diff --git a/samples/taskforge/package.json b/samples/taskforge/package.json index 5a4c26c6d..551a0466f 100644 --- a/samples/taskforge/package.json +++ b/samples/taskforge/package.json @@ -1,6 +1,6 @@ { "name": "taskforge", - "version": "3.9.2", + "version": "3.9.3", "type": "module", "private": true, "description": "A CLI for a team collaboration / project-tracking platform, built on ZenStack v3 (ORM) and better-auth.", diff --git a/tests/e2e/package.json b/tests/e2e/package.json index b38c2512b..f27191aff 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.9.2", + "version": "3.9.3", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/package.json b/tests/regression/package.json index ceaf71ba6..381c1b1d2 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.9.2", + "version": "3.9.3", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/bun/package.json b/tests/runtimes/bun/package.json index 1820347d6..1aa24913e 100644 --- a/tests/runtimes/bun/package.json +++ b/tests/runtimes/bun/package.json @@ -1,6 +1,6 @@ { "name": "bun-e2e", - "version": "3.9.2", + "version": "3.9.3", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/edge-runtime/package.json b/tests/runtimes/edge-runtime/package.json index a44893a67..63e6dd5fa 100644 --- a/tests/runtimes/edge-runtime/package.json +++ b/tests/runtimes/edge-runtime/package.json @@ -1,6 +1,6 @@ { "name": "edge-runtime-e2e", - "version": "3.9.2", + "version": "3.9.3", "private": true, "type": "module", "scripts": { From 0ad1dfb2ea9b890207069add8f764df124ddda0b Mon Sep 17 00:00:00 2001 From: FTB_lag Date: Mon, 31 Aug 2026 15:50:08 +0300 Subject: [PATCH 4/4] fix(orm): append table for order by node (#2822) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- .../orm/src/client/executor/name-mapper.ts | 47 ++++++++++++++ tests/regression/test/issue-2821.test.ts | 62 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 tests/regression/test/issue-2821.test.ts diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index b4ac42d9c..a5891a7e4 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -14,6 +14,7 @@ import { InsertQueryNode, type OperationNode, OperationNodeTransformer, + type OrderByItemNode, PrimitiveValueListNode, type QueryId, ReferenceNode, @@ -185,6 +186,52 @@ export class QueryNameMapper extends OperationNodeTransformer { }; } + protected override transformOrderByItem(node: OrderByItemNode, queryId?: QueryId) { + const result = super.transformOrderByItem(node, queryId); + return { ...result, orderBy: this.qualifyShadowedOrderByRef(result.orderBy) }; + } + + // When a column's enum type has `@map`-ed values, selecting it emits a computed + // `CASE ... END AS "column"` projection. In SQL, an unqualified `ORDER BY column` resolves + // to that output alias rather than the underlying column, silently switching the sort from + // native enum order to alphabetical order of the mapped-back labels. Re-qualify such + // references with their resolved table/alias so they keep pointing at the real column. + private qualifyShadowedOrderByRef(node: OperationNode): OperationNode { + let columnName: string | undefined; + if (ReferenceNode.is(node) && ColumnNode.is(node.column) && !node.table) { + columnName = node.column.column.name; + } else if (ColumnNode.is(node)) { + columnName = node.column.name; + } + if (!columnName) { + return node; + } + + const scope = this.resolveFieldFromScopes(columnName); + if (!scope?.model) { + return node; + } + + // we're inspecting a post-transform name: a renamed field's reference has already been + // rewritten to its column name, so if the resolved field's column differs from the name + // we're holding, the resolution is a name collision with an unrelated (renamed) field — + // qualifying based on it could point at the wrong table + if (this.mapFieldName(scope.model, columnName) !== columnName) { + return node; + } + + // and only when the enum-value mapping actually rewrites the projection + const fieldDef = getField(this.schema, scope.model, columnName); + const enumDef = fieldDef && getEnum(this.schema, fieldDef.type); + if (!enumDef || Object.keys(this.getEnumValueMapping(enumDef)).length === 0) { + return node; + } + + const tableName = + scope.alias && IdentifierNode.is(scope.alias) ? scope.alias.name : this.mapTableName(scope.model); + return ReferenceNode.create(ColumnNode.create(columnName), TableNode.create(tableName)); + } + protected override transformReference(node: ReferenceNode, queryId?: QueryId) { if (!ColumnNode.is(node.column)) { return super.transformReference(node, queryId); diff --git a/tests/regression/test/issue-2821.test.ts b/tests/regression/test/issue-2821.test.ts new file mode 100644 index 000000000..1bedeb33a --- /dev/null +++ b/tests/regression/test/issue-2821.test.ts @@ -0,0 +1,62 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// https://github.com/zenstackhq/zenstack/issues/2821 +describe('Regression for issue #2821', () => { + it('supported enum array', async () => { + const schema = ` +enum OkStatus { + OK @map("ok") + NO @map("no") + + @@map("ok_status") +} + +model Post { + id Int @id + status OkStatus +} +`; + + const db = await createTestClient(schema, { usePrismaPush: true, provider: 'postgresql', debug: true }); + + await db.post.create({ data: { id: 1, status: 'NO' } }); + await db.post.create({ data: { id: 2, status: 'OK' } }); + await db.post.create({ data: { id: 3, status: 'NO' } }); + await db.post.create({ data: { id: 4, status: 'OK' } }); + + const ascVariant1 = await db.$qb.selectFrom('Post').select('status').orderBy('status', 'asc').execute(); + const ascVariant2 = await db.$qb.selectFrom('Post').select('status').orderBy('Post.status', 'asc').execute(); + const ascVariant3 = await db.$qb + .selectFrom('Post') + .select('status as otherName') + .orderBy('status', 'asc') + .execute(); + + expect(ascVariant1).toEqual([{ status: 'OK' }, { status: 'OK' }, { status: 'NO' }, { status: 'NO' }]); + expect(ascVariant2).toEqual([{ status: 'OK' }, { status: 'OK' }, { status: 'NO' }, { status: 'NO' }]); + expect(ascVariant3).toEqual([ + { otherName: 'OK' }, + { otherName: 'OK' }, + { otherName: 'NO' }, + { otherName: 'NO' }, + ]); + + const descVariant1 = await db.$qb.selectFrom('Post').select('status').orderBy('status', 'desc').execute(); + const descVariant2 = await db.$qb.selectFrom('Post').select('status').orderBy('Post.status', 'desc').execute(); + const descVariant3 = await db.$qb + .selectFrom('Post') + .select('status as otherName') + .orderBy('status', 'desc') + .execute(); + + expect(descVariant1).toEqual([{ status: 'NO' }, { status: 'NO' }, { status: 'OK' }, { status: 'OK' }]); + expect(descVariant2).toEqual([{ status: 'NO' }, { status: 'NO' }, { status: 'OK' }, { status: 'OK' }]); + expect(descVariant3).toEqual([ + { otherName: 'NO' }, + { otherName: 'NO' }, + { otherName: 'OK' }, + { otherName: 'OK' }, + ]); + }); +});