Repository navigation
Description
A @computed field referenced from its own model's @@allow/@@deny policy expression compiles to a plain "Model"."field" column reference, which doesn't physically exist in the database — the write fails with "Unknown column" / "no such column" instead of evaluating the computed expression.
Repro
const db = await createPolicyTestClient(
`
model Model {
id Int @id @default(autoincrement())
x Int
canEdit Boolean @computed
@@allow('create,read', true)
@@allow('update', canEdit)
}
`,
{
provider: 'mysql',
usePrismaPush: true,
computedFields: { model: { canEdit: (eb) => eb.lit(true) } },
},
);
await db.model.create({ data: { id: 1, x: 1 } });
await db.model.update({ where: { id: 1 }, data: { x: 2 } }); // throwsThrows (MySQL):
Unknown column 'Model.canEdit' in 'where clause'
sql: 'update `Model` set `x` = ? where `Model`.`id` = ? and `Model`.`canEdit` '
Not dialect-specific — every other query path (plain select/where/orderBy) already inlines @computed fields correctly via BaseCrudDialect.fieldRef(); the policy expression transformer is the one place that doesn't.
Root cause
ExpressionTransformer.createColumnRef (packages/plugins/policy/src/expression-transformer.ts) builds a bare column ref for every non-relation field, with no check for fieldDef.computed:
private createColumnRef(column: string, context: ExpressionTransformerContext) {
const tableName = context.alias ?? context.modelOrType;
const fieldDef = QueryUtils.requireField(this.schema, context.modelOrType, column);
if (!fieldDef.originModel || fieldDef.originModel === context.modelOrType) {
return ReferenceNode.create(ColumnNode.create(column), TableNode.create(tableName)); // <-- bug
}
return this.buildDelegateBaseFieldSelect(...);
}The transformer already has this.dialect (a BaseCrudDialect instance, built at construction and used elsewhere in this same file) whose fieldRef(model, field, alias) correctly inlines @computed fields everywhere else — createColumnRef just never delegates to it.
Fix
When the referenced field is computed (and not parameterized — a parameterized computed field needs query-time args a static policy expression can't supply), delegate to this.dialect.fieldRef() and bridge its Expression back to an OperationNode:
private createColumnRef(column: string, context: ExpressionTransformerContext) {
const tableName = context.alias ?? context.modelOrType;
const fieldDef = QueryUtils.requireField(this.schema, context.modelOrType, column);
+
+ if (fieldDef.computed) {
+ if (fieldDef.params) {
+ throw new Error(`parameterized computed field "${column}" cannot be used in a policy expression`);
+ }
+ return this.dialect.fieldRef(context.modelOrType, column, tableName).toOperationNode();
+ }
+
if (!fieldDef.originModel || fieldDef.originModel === context.modelOrType) {
return ReferenceNode.create(ColumnNode.create(column), TableNode.create(tableName));
}
return this.buildDelegateBaseFieldSelect(...);
}PR incoming.
Scope
This only fixes a computed field referenced from its own model's policy. A computed field reached by traversing a relation (e.g. a SubmissionScore policy referencing submission.someComputedField) goes through a different code path (transformRelationAccess) and is not covered by this fix — a separate, still-open gap.
Environment
@zenstackhq/plugin-policyatdevHEAD151524b- Reproduces on any dialect — not MySQL-specific (unlike loadAfterMutationEntities builds invalid WHERE (id) IN () for a zero-match updateMany/deleteMany when any afterEntityMutation hook is registered (MySQL) #2884); MySQL used in the repro only because it's already set up in this environment.
Activity
ymc9 commentedon Nov 6, 2026
Fixed in v3.10.1 by #2889 and #2910.