Skip to content

Comments

fix: cast auth() string values to uuid on PostgreSQL when field has @db.Uuid#2396

Open
niehaus1301 wants to merge 1 commit intozenstackhq:devfrom
niehaus1301:fix/postgres-uuid-auth-cast
Open

fix: cast auth() string values to uuid on PostgreSQL when field has @db.Uuid#2396
niehaus1301 wants to merge 1 commit intozenstackhq:devfrom
niehaus1301:fix/postgres-uuid-auth-cast

Conversation

@niehaus1301
Copy link

@niehaus1301 niehaus1301 commented Feb 22, 2026

Problem

When using auth() in access policies with PostgreSQL and the auth model has fields annotated with @db.Uuid, the policy check fails with:

operator does not exist: text = uuid

This happens because parameterized string values from auth() are sent as text type, but the corresponding database columns are uuid type. PostgreSQL does not implicitly cast between these types.

Reproduction

model User {
  userId String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  email  String @unique
  // ...
  @@auth()
}

model Client {
  clientId     String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  dealershipId String @db.Uuid
  // ...
  @@allow('read', auth().dealershipId == dealershipId)
}

The generated SQL compares auth().dealershipId (a text parameter) against the dealershipId column (uuid type), which PostgreSQL rejects.

Root Cause

In expression-transformer.ts, the valueMemberAccess method resolves auth() member fields and calls transformValue(curr, currType) where currType is the ZModel type ('String'). The PostgreSQL dialect's transformInput for String is a no-op, so the value remains a plain text parameter. When Kysely parameterizes this as $1, PostgreSQL infers text type, causing the type mismatch.

The @db.Uuid native type attribute is available in the runtime schema via FieldDef.attributes, but was not being utilized during value transformation.

Fix

Added an applyNativeTypeCast method that inspects the field's native type attributes and wraps the value node with an explicit ::uuid SQL cast when:

  1. The provider is PostgreSQL, AND
  2. The field has the @db.Uuid attribute

The cast is applied in two code paths:

  • valueMemberAccess: For auth().field member access in policies (the primary case)
  • _field with contextValue: For field evaluation in collection predicates with value objects

Changes

Only one file is modified: packages/plugins/policy/src/expression-transformer.ts

  • Added sql to Kysely imports
  • Modified valueMemberAccess to store the full FieldDef and apply native type cast after transformValue
  • Modified _field with contextValue to also apply native type cast
  • Added applyNativeTypeCast helper with JSDoc documentation

Fixes #2394

Summary by CodeRabbit

Bug Fixes

  • Fixed policy expression evaluation to correctly handle UUID column comparisons in PostgreSQL databases. Policy expressions now properly cast values when evaluating against UUID-typed fields.

…db.Uuid

When policy expressions compare auth() member values against columns
with @db.Uuid native type, PostgreSQL raises 'operator does not exist:
text = uuid' because parameterized string values are sent as text type.

This fix inspects the field's native type attributes from the runtime
schema and applies an explicit ::uuid cast when needed. The cast is
applied in two code paths:
- valueMemberAccess: for auth().field member access in policies
- _field with contextValue: for field evaluation in collection predicates

Fixes zenstackhq#2394
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

This PR adds native database type casting support to policy expression evaluation, specifically handling PostgreSQL UUID casts. A new private helper method applyNativeTypeCast is introduced to wrap value nodes with explicit UUID casts (::uuid) for fields annotated with @db.Uuid, ensuring correct SQL comparisons between string values and UUID columns.

Changes

Cohort / File(s) Summary
PostgreSQL UUID Cast Support
packages/plugins/policy/src/expression-transformer.ts
Added private applyNativeTypeCast helper method to apply ::uuid casts for PostgreSQL fields marked with @db.Uuid. Updated value transformation call sites to apply casts and reordered type-deduction logic to fetch field definitions before type determination.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A UUID cast, so clever and fine,
Makes PostgreSQL comparisons align,
No more "text = uuid" despair,
The rabbit fixed casting with care!
::uuid wrapped values take flight,
Policy checks now feel just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding UUID casting for auth() string values on PostgreSQL with @db.Uuid fields.
Linked Issues check ✅ Passed The pull request fully addresses issue #2394 by implementing native DB type casting support for policy expressions, detecting @db.Uuid fields, and wrapping values with explicit ::uuid casts in PostgreSQL.
Out of Scope Changes check ✅ Passed All changes are scoped to expression-transformer.ts and directly address the UUID casting issue described in #2394 without introducing unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@niehaus1301 niehaus1301 marked this pull request as ready for review February 22, 2026 02:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/plugins/policy/src/expression-transformer.ts`:
- Around line 829-836: The applyNativeTypeCast method currently always appends
"::uuid" for fields with the `@db.Uuid` attribute, which breaks when
fieldDef.array is true because arrays must be cast to "uuid[]"; update
applyNativeTypeCast to check fieldDef.array and, when provider is 'postgresql'
and the field has the `@db.Uuid` attribute, return sql`${new
ExpressionWrapper(node)}::uuid[]` for arrays and sql`${new
ExpressionWrapper(node)}::uuid` for scalars (preserving existing behavior for
non-array fields) so array values produced by buildArrayValue/transformValue are
cast correctly.

Comment on lines +829 to +836
private applyNativeTypeCast(node: OperationNode, fieldDef: FieldDef): OperationNode {
if (this.schema.provider.type === 'postgresql' && fieldDef.attributes) {
if (fieldDef.attributes.some((attr) => attr.name === '@db.Uuid')) {
return sql`${new ExpressionWrapper(node)}::uuid`.toOperationNode();
}
}
return node;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Array UUID field (fieldDef.array) not checked — wrong cast type for array fields.

When a field is declared as String[] @db.Uuid``, transformValue returns an array node (via `buildArrayValue`). Wrapping it with `::uuid` produces `ARRAY[$1, $2]::uuid`, which PostgreSQL rejects — the correct cast is `::uuid[]`. The condition needs to branch on `fieldDef.array`.

🐛 Proposed fix
 private applyNativeTypeCast(node: OperationNode, fieldDef: FieldDef): OperationNode {
     if (this.schema.provider.type === 'postgresql' && fieldDef.attributes) {
         if (fieldDef.attributes.some((attr) => attr.name === '@db.Uuid')) {
-            return sql`${new ExpressionWrapper(node)}::uuid`.toOperationNode();
+            const castType = fieldDef.array ? 'uuid[]' : 'uuid';
+            return sql`${new ExpressionWrapper(node)}::${sql.raw(castType)}`.toOperationNode();
         }
     }
     return node;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private applyNativeTypeCast(node: OperationNode, fieldDef: FieldDef): OperationNode {
if (this.schema.provider.type === 'postgresql' && fieldDef.attributes) {
if (fieldDef.attributes.some((attr) => attr.name === '@db.Uuid')) {
return sql`${new ExpressionWrapper(node)}::uuid`.toOperationNode();
}
}
return node;
}
private applyNativeTypeCast(node: OperationNode, fieldDef: FieldDef): OperationNode {
if (this.schema.provider.type === 'postgresql' && fieldDef.attributes) {
if (fieldDef.attributes.some((attr) => attr.name === '@db.Uuid')) {
const castType = fieldDef.array ? 'uuid[]' : 'uuid';
return sql`${new ExpressionWrapper(node)}::${sql.raw(castType)}`.toOperationNode();
}
}
return node;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugins/policy/src/expression-transformer.ts` around lines 829 -
836, The applyNativeTypeCast method currently always appends "::uuid" for fields
with the `@db.Uuid` attribute, which breaks when fieldDef.array is true because
arrays must be cast to "uuid[]"; update applyNativeTypeCast to check
fieldDef.array and, when provider is 'postgresql' and the field has the `@db.Uuid`
attribute, return sql`${new ExpressionWrapper(node)}::uuid[]` for arrays and
sql`${new ExpressionWrapper(node)}::uuid` for scalars (preserving existing
behavior for non-array fields) so array values produced by
buildArrayValue/transformValue are cast correctly.

Copy link
Member

@ymc9 ymc9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @niehaus1301 , thanks for reporting the issue and making the PR.

The policy handling code is supposed to be db-type agnostic. I understand the problem now and will probably attempt a different fix at the db dialect side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PostgreSQL: policy check fails with "operator does not exist: text = uuid" when auth() field has @db.Uuid

2 participants