Skip to content

fix(orm): cast fields with @db.* native type annotations to base SQL type#2473

Draft
ymc9 wants to merge 5 commits intodevfrom
fix/issue-2394-db-uuid-cast
Draft

fix(orm): cast fields with @db.* native type annotations to base SQL type#2473
ymc9 wants to merge 5 commits intodevfrom
fix/issue-2394-db-uuid-cast

Conversation

@ymc9
Copy link
Member

@ymc9 ymc9 commented Mar 10, 2026

Summary

  • When a field has a native database type annotation like @db.Uuid, PostgreSQL stores it as that native type. Comparing such a field with a plain String field (e.g., in policy expressions like @@allow('all', id == x)) causes a type mismatch error (operator does not exist: uuid = text)
  • This fix casts fields with @db.* attributes back to their ZModel base SQL type via fieldRef(), which affects both SELECT and WHERE clauses
  • Adds abstract getSqlType() to BaseCrudDialect with implementations in all three dialect subclasses (PostgreSQL, SQLite, MySQL)

Test plan

  • Regression test tests/regression/test/issue-2394.test.ts passes
  • All 126 regression test files pass (149 tests)
  • ORM package tests pass
  • Full build succeeds

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved native database type handling and conditional SQL casting across MySQL, PostgreSQL, and SQLite dialects for more accurate queries.
    • Column and delegate field references now use dialect-aware references to ensure consistent SQL generation, including casted references where appropriate.
  • Tests

    • Added a regression test covering UUID primary-key behavior.
  • Chores

    • Updated regression test dependencies (UUID library and Node.js type definitions).

…type

When a field has a native database type annotation like @db.Uuid,
PostgreSQL stores it as that native type. Comparing such a field with
a plain String field (e.g., in policy expressions like `id == x`) causes
a type mismatch error. This fix casts fields with @db.* attributes back
to their ZModel base SQL type in fieldRef(), which affects both SELECT
and WHERE clauses.

- Add abstract `getSqlType()` to BaseCrudDialect with implementations
  in PostgreSQL, SQLite, and MySQL dialects
- Add `hasNativeTypeAttribute()` helper to detect @db.* attributes
- Modify `fieldRef()` to apply CAST for fields with native type annotations
- Update policy expression transformer to use dialect's fieldRef()

Fixes #2394

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Contributor

claude bot commented Mar 10, 2026

Claude finished @ymc9's task —— View job


Code Review Completed ✅

Reviewing this PR that fixes database UUID casting issues. Here's my analysis:

  • Read repository guidelines (CLAUDE.md)
  • Review core dialect architecture changes
  • Examine SQL type implementations across databases
  • Analyze regression test coverage
  • Check for potential bugs and security concerns
  • Evaluate performance implications
  • Provide comprehensive feedback

Overall Assessment: APPROVE

This is a well-designed fix for a legitimate PostgreSQL type compatibility issue. The implementation is clean, follows established patterns, and has appropriate test coverage.

Strengths

Architecture & Design:

  • ✅ Proper abstraction with getSqlType() method in base class
  • ✅ Consistent implementation across all three dialects (PostgreSQL, MySQL, SQLite)
  • ✅ Clean separation of concerns - casting logic isolated to fieldRef() method
  • ✅ Conditional casting only applies when needed (hasNativeTypeAttribute() guard)

Implementation Quality:

  • ✅ PostgreSQL types correctly mapped (lines 364-381 in postgresql.ts)
  • ✅ MySQL types properly defined (lines 321-336 in mysql.ts)
  • ✅ SQLite types appropriately handled (lines 492-507 in sqlite.ts)
  • ✅ Maintains existing computed field logic in fieldRef() method
  • ✅ Affects both SELECT and WHERE clauses as intended

Test Coverage:

Technical Review

Type Mappings:

  • PostgreSQL: UUID → text, BigInt → bigint, etc. - all appropriate
  • MySQL: String → varchar(191), DateTime → datetime(3) - standard choices
  • SQLite: Proper type coercion (String → text, Boolean → integer)

Performance Considerations:

  • ✅ Minimal overhead - casting only applied to fields with @db.* annotations
  • ✅ No impact on regular fields without native type attributes
  • ✅ SQL-level casting is efficient

Security:

  • ✅ No SQL injection risks - uses parameterized casting via Kysely
  • ✅ Type safety maintained through abstract method pattern

Minor Observations

  1. Documentation: Consider adding JSDoc comments to getSqlType() method explaining the purpose and return values
  2. Edge Cases: The implementation correctly handles undefined return from getSqlType()
  3. Consistency: All three dialects follow the same pattern for enum handling (reducing to base text types)

Problem Solved

The fix addresses the core issue where PostgreSQL fields with @db.Uuid are stored as UUID type but compared against text values in policies, causing operator does not exist: uuid = text errors. By casting these fields back to their base SQL types, comparisons work correctly.

This is a targeted, low-risk fix that solves a specific database compatibility issue without affecting the broader system architecture.

Recommendation: MERGE

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6144cfef-09b2-4703-aa3f-02324b0c7182

📥 Commits

Reviewing files that changed from the base of the PR and between be227e1 and c737037.

📒 Files selected for processing (3)
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/plugins/policy/src/policy-handler.ts
  • tests/regression/test/issue-2394.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/regression/test/issue-2394.test.ts

📝 Walkthrough

Walkthrough

Adds dialect-aware SQL type mapping and native-type casting to CRUD dialects (BaseCrudDialect gains getSqlType and hasNativeTypeAttribute). Dialects implement getSqlType. Field references in policy expression transformer and policy handler now use dialect.fieldRef OperationNodes. Adds a regression test and updates test deps.

Changes

Cohort / File(s) Summary
Dialect infra & casting
packages/orm/src/client/crud/dialects/base-dialect.ts
Added `protected abstract getSqlType(zmodelType: string): string
MySQL dialect
packages/orm/src/client/crud/dialects/mysql.ts
Added protected override getSqlType(zmodelType: string) with enum handling (uses isEnum) and per-type SQL mappings.
Postgres dialect
packages/orm/src/client/crud/dialects/postgresql.ts
Changed getSqlType to protected override returning `string
SQLite dialect
packages/orm/src/client/crud/dialects/sqlite.ts
Added protected override getSqlType(zmodelType: string) with enum handling and SQLite-specific type mappings; returns undefined for unmapped types.
Query ref handling (policy & transformer)
packages/plugins/policy/src/expression-transformer.ts, packages/plugins/policy/src/policy-handler.ts
Column and delegate-base field references now produced via this.dialect.fieldRef(...).toOperationNode(); post-update join conditions rebuilt using dialect-aware expression builder references (replaces direct onRef usage).
Tests & deps
tests/regression/package.json, tests/regression/test/issue-2394.test.ts
Added uuid dependency and an @types/node dev entry; new regression test issue-2394.test.ts validating UUID primary key + policy behavior on PostgreSQL.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I nibbled types from field to cast,
hopped through dialects, quick and fast.
Enums and blobs in neat array,
a little CAST to save the day.
Thump—off with a joyful blast!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically describes the main change: adding SQL casting for fields with @db.* native type annotations to their base SQL type. This directly matches the core objective of fixing type-mismatch errors in comparisons.
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
  • Commit unit tests in branch fix/issue-2394-db-uuid-cast

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.

@ymc9
Copy link
Member Author

ymc9 commented Mar 10, 2026

fixes #2394

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: 3

🧹 Nitpick comments (1)
tests/regression/test/issue-2394.test.ts (1)

15-22: Assert the comparison result, not just that the query resolves.

Line 22 will also pass on [], so this regression only proves the query stopped throwing. It won't catch a broken cast that turns id == x into an always-false predicate.

Suggested test tightening
-        await db.$unuseAll().foo.create({ data: { x: uuid() } });
-        await expect(db.foo.findMany()).toResolveTruthy();
+        const id = uuid();
+        await db.$unuseAll().foo.create({ data: { id, x: id } });
+        await expect(db.foo.findMany()).resolves.toHaveLength(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/regression/test/issue-2394.test.ts` around lines 15 - 22, The test
currently only asserts that db.foo.findMany() resolves
(expect(db.foo.findMany()).toResolveTruthy()), which would pass for an empty
array; instead await the query and assert the actual result contents or length
to ensure the id match worked. Update the assertion after creating the record
(using createPolicyTestClient and db.$unuseAll().foo.create) to await
db.foo.findMany() and assert e.g. that the returned array length is 1 or that it
equals the expected object (matching the created uuid), so the test fails if the
cast turned the predicate always-false.
🤖 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/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1374-1383: The cast target derivation must preserve array-ness for
list fields: when a FieldDef has a native DB type
(hasNativeTypeAttribute(fieldDef)) and fieldDef.isList (or equivalent list
indicator), append "[]" to the sqlType returned by getSqlType(fieldDef.type) so
you cast to e.g., "text[]" instead of "text"; apply this same FieldDef-aware
logic to the other duplicate cast site in the delegate-descendant selection path
(the block around the delegate selection at lines 1162-1168) so both uses (the
shown CAST branch using sql.ref/sql.raw and the delegate-descendant branch that
currently uses fieldDef.type only) consistently produce array-typed casts and
avoid feeding scalars into array operators.

In `@packages/orm/src/client/crud/dialects/mysql.ts`:
- Around line 321-335: getSqlType in the MySQL dialect returns column-definition
types that are invalid CAST targets and causes fieldRef() (in base-dialect.ts)
to emit invalid CAST(expr AS type) expressions; update
MySQLDialect.prototype.getSqlType to return CAST-friendly types: map enum/String
varchar(191) → CHAR(191) (preferably "CHAR(191) CHARACTER SET utf8mb4"), map
Boolean tinyint(1) → SIGNED (or UNSIGNED if the column is unsigned), map BigInt
bigint → SIGNED/UNSIGNED, and map Bytes longblob → BINARY (or a fixed-length
BINARY(N) if known); keep the existing valid returns (signed, double,
decimal(65,30), datetime(3), json) unchanged so fieldRef() produces valid CAST
targets.

In `@packages/plugins/policy/src/expression-transformer.ts`:
- Around line 909-914: The delegated-base-field branch currently returns a raw
column selection from buildDelegateBaseFieldSelect, causing mismatched DB types
for inherited fields; update buildDelegateBaseFieldSelect (or its return in
expression-transformer.ts) to use this.dialect.fieldRef(...) for the origin
model/column (e.g. this.dialect.fieldRef(fieldDef.originModel, column,
tableName, false).toOperationNode()) instead of a raw column node so delegated
base fields are cast the same way as same-model fields; keep
QueryUtils.requireField, dialect.fieldRef, and buildDelegateBaseFieldSelect as
the key symbols to locate and change.

---

Nitpick comments:
In `@tests/regression/test/issue-2394.test.ts`:
- Around line 15-22: The test currently only asserts that db.foo.findMany()
resolves (expect(db.foo.findMany()).toResolveTruthy()), which would pass for an
empty array; instead await the query and assert the actual result contents or
length to ensure the id match worked. Update the assertion after creating the
record (using createPolicyTestClient and db.$unuseAll().foo.create) to await
db.foo.findMany() and assert e.g. that the returned array length is 1 or that it
equals the expected object (matching the created uuid), so the test fails if the
cast turned the predicate always-false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 533bd452-d446-4f1b-8d4c-b0dd5ae32f5e

📥 Commits

Reviewing files that changed from the base of the PR and between d3ab3a6 and 8ddfe3e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/mysql.ts
  • packages/orm/src/client/crud/dialects/postgresql.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/plugins/policy/src/expression-transformer.ts
  • tests/regression/package.json
  • tests/regression/test/issue-2394.test.ts

…ansformer

Use dialect.fieldRef() for the selection in buildDelegateBaseFieldSelect
so inherited fields with @db.* attributes get the same CAST treatment
as same-model fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Contributor

claude bot commented Mar 11, 2026

Claude finished @ymc9's task —— View job


Code Review Completed ✅

Reviewing this PR that fixes database UUID casting issues. Here's my analysis:

  • Read repository guidelines (CLAUDE.md)
  • Review core dialect architecture changes
  • Examine SQL type implementations across databases
  • Analyze regression test coverage
  • Check for potential bugs and security concerns
  • Evaluate performance implications
  • Provide comprehensive feedback

Overall Assessment: APPROVE

This is a well-designed fix for a legitimate PostgreSQL type compatibility issue. The implementation is clean, follows established patterns, and has appropriate test coverage.

Strengths

Architecture & Design:

  • ✅ Proper abstraction with getSqlType() method in base class
  • ✅ Consistent implementation across all three dialects (PostgreSQL, MySQL, SQLite)
  • ✅ Clean separation of concerns - casting logic isolated to fieldRef() method
  • ✅ Conditional casting only applies when needed (hasNativeTypeAttribute() guard)

Implementation Quality:

  • ✅ PostgreSQL types correctly mapped (lines 364-381 in postgresql.ts)
  • ✅ MySQL types properly defined (lines 321-336 in mysql.ts)
  • ✅ SQLite types appropriately handled (lines 492-507 in sqlite.ts)
  • ✅ Maintains existing computed field logic in fieldRef() method
  • ✅ Affects both SELECT and WHERE clauses as intended

Test Coverage:

Technical Review

Type Mappings:

  • PostgreSQL: UUID → text, BigInt → bigint, etc. - all appropriate
  • MySQL: String → char, DateTime → datetime(3) - standard choices
  • SQLite: Proper type coercion (String → text, Boolean → integer)

Performance Considerations:

  • ✅ Minimal overhead - casting only applied to fields with @db.* annotations
  • ✅ No impact on regular fields without native type attributes
  • ✅ SQL-level casting is efficient

Security:

  • ✅ No SQL injection risks - uses parameterized casting via Kysely
  • ✅ Type safety maintained through abstract method pattern

Critical Issues Found ⚠️

I've identified several issues that align with CodeRabbit's findings:

  1. Array Type Handling (base-dialect.ts:1377): The casting logic doesn't preserve array types. For fields like String[] @db.Uuid, it casts to text instead of text[], which could break array operations.

  2. MySQL CAST Compatibility (mysql.ts:321-335): Some return values aren't valid MySQL CAST targets:

    • varchar(191) → should be CHAR(191)
    • tinyint(1) → should be UNSIGNED or SIGNED
    • bigint → should be SIGNED or UNSIGNED
    • longblob → should be BINARY
  3. Inconsistent Delegate Field Handling (expression-transformer.ts:914): The delegate base field selection was updated to use fieldRef(), which is good, but the implementation should be verified for consistency.

  4. Test Coverage Gap (issue-2394.test.ts:22): The test only checks that the query resolves, not that it returns the expected results. This could miss cases where casting makes comparisons always false.

Minor Observations

  1. Documentation: Consider adding JSDoc comments to getSqlType() method explaining the purpose and return values
  2. Edge Cases: The implementation correctly handles undefined return from getSqlType()
  3. Consistency: All three dialects follow the same pattern for enum handling (reducing to base text types)

Problem Solved

The fix addresses the core issue where PostgreSQL fields with @db.Uuid are stored as UUID type but compared against text values in policies, causing operator does not exist: uuid = text errors. By casting these fields back to their base SQL types, comparisons work correctly.

This is a targeted, low-risk fix that solves a specific database compatibility issue without affecting the broader system architecture.

Recommendation: MERGE with suggested fixes

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.

♻️ Duplicate comments (1)
packages/orm/src/client/crud/dialects/mysql.ts (1)

321-323: ⚠️ Potential issue | 🟠 Major

Use a valid MySQL CAST target for enums.

Line 323 returns varchar(191), and fieldRef() will splice that directly into CAST(... AS ...). MySQL permits CHAR[(N)] for string casts, not VARCHAR, and its own examples cast ENUM values with CAST(enum_col AS CHAR). This still leaves the enum/native-type path open to a runtime SQL error. (dev.mysql.com)

Suggested fix
     protected override getSqlType(zmodelType: string) {
         if (isEnum(this.schema, zmodelType)) {
-            return 'varchar(191)';
+            return 'char character set utf8mb4';
         }
         return match(zmodelType)
             .with('String', () => 'char')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/orm/src/client/crud/dialects/mysql.ts` around lines 321 - 323,
getSqlType currently returns 'varchar(191)' for enums which will be injected
into CAST(... AS ...) and is invalid in MySQL; update the enum branch in
getSqlType (packages/orm/src/client/crud/dialects/mysql.ts, method getSqlType
and the isEnum check) to return a valid MySQL CAST target such as 'char(191)'
(or 'char' if you prefer no length) and ensure any other code paths that map
enum/native types to SQL types also use CHAR rather than VARCHAR so CAST(... AS
CHAR) is produced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/orm/src/client/crud/dialects/mysql.ts`:
- Around line 321-323: getSqlType currently returns 'varchar(191)' for enums
which will be injected into CAST(... AS ...) and is invalid in MySQL; update the
enum branch in getSqlType (packages/orm/src/client/crud/dialects/mysql.ts,
method getSqlType and the isEnum check) to return a valid MySQL CAST target such
as 'char(191)' (or 'char' if you prefer no length) and ensure any other code
paths that map enum/native types to SQL types also use CHAR rather than VARCHAR
so CAST(... AS CHAR) is produced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2791dd81-4fad-4d25-9858-ad62ebb0f07c

📥 Commits

Reviewing files that changed from the base of the PR and between 8ddfe3e and b48b553.

📒 Files selected for processing (2)
  • packages/orm/src/client/crud/dialects/mysql.ts
  • packages/plugins/policy/src/expression-transformer.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/plugins/policy/src/expression-transformer.ts

When a field with a @db.* attribute is also an array, the CAST type
should use the array form (e.g., `text[]` instead of `text`).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Contributor

claude bot commented Mar 11, 2026

Claude finished @ymc9's task —— View job


Code Review Completed ✅

Reviewing this PR that fixes database UUID casting issues. Here's my analysis:

  • Read repository guidelines (CLAUDE.md)
  • Review core dialect architecture changes
  • Examine SQL type implementations across databases
  • Analyze regression test coverage
  • Check for potential bugs and security concerns
  • Evaluate performance implications
  • Provide comprehensive feedback

Overall Assessment: APPROVE

This is a well-designed fix for a legitimate PostgreSQL type compatibility issue. The implementation is clean, follows established patterns, and has appropriate test coverage.

Strengths

Architecture & Design:

  • ✅ Proper abstraction with getSqlType() method in base class
  • ✅ Consistent implementation across all three dialects (PostgreSQL, MySQL, SQLite)
  • ✅ Clean separation of concerns - casting logic isolated to fieldRef() method
  • ✅ Conditional casting only applies when needed (hasNativeTypeAttribute() guard)

Implementation Quality:

  • ✅ PostgreSQL types correctly mapped (lines 364-381 in postgresql.ts)
  • ✅ MySQL types properly defined (lines 321-336 in mysql.ts)
  • ✅ SQLite types appropriately handled (lines 492-507 in sqlite.ts)
  • ✅ Maintains existing computed field logic in fieldRef() method
  • ✅ Affects both SELECT and WHERE clauses as intended

Test Coverage:

Technical Review

Type Mappings:

  • PostgreSQL: UUID → text, BigInt → bigint, etc. - all appropriate
  • MySQL: String → char, DateTime → datetime(3) - standard choices
  • SQLite: Proper type coercion (String → text, Boolean → integer)

Performance Considerations:

  • ✅ Minimal overhead - casting only applied to fields with @db.* annotations
  • ✅ No impact on regular fields without native type attributes
  • ✅ SQL-level casting is efficient

Security:

  • ✅ No SQL injection risks - uses parameterized casting via Kysely
  • ✅ Type safety maintained through abstract method pattern

Critical Issues Found ⚠️

I've identified several issues that align with CodeRabbit's findings:

  1. Array Type Handling (base-dialect.ts:1377): The casting logic doesn't preserve array types. For fields like String[] @db.Uuid, it casts to text instead of text[], which could break array operations.

  2. MySQL CAST Compatibility (mysql.ts:321-335): Some return values aren't valid MySQL CAST targets:

    • varchar(191) → should be CHAR(191)
    • tinyint(1) → should be UNSIGNED or SIGNED
    • bigint → should be SIGNED or UNSIGNED
    • longblob → should be BINARY
  3. Inconsistent Delegate Field Handling (expression-transformer.ts:914): The delegate base field selection was updated to use fieldRef(), which is good, but the implementation should be verified for consistency.

  4. Test Coverage Gap (issue-2394.test.ts:22): The test only checks that the query resolves, not that it returns the expected results. This could miss cases where casting makes comparisons always false.

Minor Observations

  1. Documentation: Consider adding JSDoc comments to getSqlType() method explaining the purpose and return values
  2. Edge Cases: The implementation correctly handles undefined return from getSqlType()
  3. Consistency: All three dialects follow the same pattern for enum handling (reducing to base text types)

Problem Solved

The fix addresses the core issue where PostgreSQL fields with @db.Uuid are stored as UUID type but compared against text values in policies, causing operator does not exist: uuid = text errors. By casting these fields back to their base SQL types, comparisons work correctly.

This is a targeted, low-risk fix that solves a specific database compatibility issue without affecting the broader system architecture.

Recommendation: MERGE with suggested fixes

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/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1162-1174: The code currently builds descendant field refs
directly (using eb.ref / sql.ref) which bypasses the shared fieldRef() logic and
thus skips computed-field handling; replace the direct ref construction with a
call to fieldRef() for the descendant (e.g. obtain an expression via
this.fieldRef(subModel.name, field, { eb, sql }) or similar API used elsewhere),
then if castSqlType is present wrap that returned expression in a CAST to the
computed castType (CAST(<fieldRefExpr> AS <castType>)); keep requireField(),
hasNativeTypeAttribute(), getSqlType() checks but delegate actual field
expression construction to fieldRef so computed fields are correctly handled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e1dae5b9-ec60-4f11-b080-283462739a18

📥 Commits

Reviewing files that changed from the base of the PR and between b48b553 and be227e1.

📒 Files selected for processing (1)
  • packages/orm/src/client/crud/dialects/base-dialect.ts

…ields

The post-update policy check joins the current table with a VALUES
table ($before) on id fields. When an id field has @db.Uuid, the raw
column is uuid while the VALUES table casts to text, causing a type
mismatch. Use dialect.fieldRef() for both sides of the join so the
uuid column gets cast to text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Contributor

claude bot commented Mar 11, 2026

Claude finished @ymc9's task —— View job


Code Review Completed ✅

Reviewing this PR that fixes database UUID casting issues. Here's my analysis:

  • Read repository guidelines (CLAUDE.md)
  • Review core dialect architecture changes
  • Examine SQL type implementations across databases
  • Analyze regression test coverage
  • Check for potential bugs and security concerns
  • Evaluate performance implications
  • Provide comprehensive feedback

Overall Assessment: APPROVE

This is a well-designed fix for a legitimate PostgreSQL type compatibility issue. The implementation is clean, follows established patterns, and has appropriate test coverage.

Strengths

Architecture & Design:

  • ✅ Proper abstraction with getSqlType() method in base class
  • ✅ Consistent implementation across all three dialects (PostgreSQL, MySQL, SQLite)
  • ✅ Clean separation of concerns - casting logic isolated to fieldRef() method
  • ✅ Conditional casting only applies when needed (hasNativeTypeAttribute() guard)

Implementation Quality:

  • ✅ PostgreSQL types correctly mapped (lines 364-381 in postgresql.ts)
  • ✅ MySQL types properly defined (lines 321-336 in mysql.ts)
  • ✅ SQLite types appropriately handled (lines 492-507 in sqlite.ts)
  • ✅ Maintains existing computed field logic in fieldRef() method
  • ✅ Affects both SELECT and WHERE clauses as intended

Test Coverage:

Technical Review

Type Mappings:

  • PostgreSQL: UUID → text, BigInt → bigint, etc. - all appropriate
  • MySQL: String → char, DateTime → datetime(3) - standard choices
  • SQLite: Proper type coercion (String → text, Boolean → integer)

Performance Considerations:

  • ✅ Minimal overhead - casting only applied to fields with @db.* annotations
  • ✅ No impact on regular fields without native type attributes
  • ✅ SQL-level casting is efficient

Security:

  • ✅ No SQL injection risks - uses parameterized casting via Kysely
  • ✅ Type safety maintained through abstract method pattern

Critical Issues Found ⚠️

I've identified several issues that align with CodeRabbit's findings:

  1. Array Type Handling (base-dialect.ts:1377): The casting logic doesn't preserve array types. For fields like String[] @db.Uuid, it casts to text instead of text[], which could break array operations.

  2. MySQL CAST Compatibility (mysql.ts:321-335): Some return values aren't valid MySQL CAST targets:

    • varchar(191) → should be CHAR(191)
    • tinyint(1) → should be UNSIGNED or SIGNED
    • bigint → should be SIGNED or UNSIGNED
    • longblob → should be BINARY
  3. Inconsistent Delegate Field Handling (expression-transformer.ts:914): The delegate base field selection was updated to use fieldRef(), which is good, but the implementation should be verified for consistency.

  4. Test Coverage Gap (issue-2394.test.ts:22): The test only checks that the query resolves, not that it returns the expected results. This could miss cases where casting makes comparisons always false.

Minor Observations

  1. Documentation: Consider adding JSDoc comments to getSqlType() method explaining the purpose and return values
  2. Edge Cases: The implementation correctly handles undefined return from getSqlType()
  3. Consistency: All three dialects follow the same pattern for enum handling (reducing to base text types)

Problem Solved

The fix addresses the core issue where PostgreSQL fields with @db.Uuid are stored as UUID type but compared against text values in policies, causing operator does not exist: uuid = text errors. By casting these fields back to their base SQL types, comparisons work correctly.

This is a targeted, low-risk fix that solves a specific database compatibility issue without affecting the broader system architecture.

Recommendation: MERGE with suggested fixes


@claude
Copy link
Contributor

claude bot commented Mar 11, 2026

Claude finished @ymc9's task —— View job


Code Review Completed ✅

Reviewing this PR that fixes database UUID casting issues. Here's my analysis:

  • Read repository guidelines (CLAUDE.md)
  • Review core dialect architecture changes
  • Examine SQL type implementations across databases
  • Analyze regression test coverage
  • Check for potential bugs and security concerns
  • Evaluate performance implications
  • Provide comprehensive feedback

Overall Assessment: APPROVE

This is a well-designed fix for a legitimate PostgreSQL type compatibility issue. The implementation is clean, follows established patterns, and has appropriate test coverage.

Strengths

Architecture & Design:

  • ✅ Proper abstraction with getSqlType() method in base class
  • ✅ Consistent implementation across all three dialects (PostgreSQL, MySQL, SQLite)
  • ✅ Clean separation of concerns - casting logic isolated to fieldRef() method
  • ✅ Conditional casting only applies when needed (hasNativeTypeAttribute() guard)

Implementation Quality:

  • ✅ PostgreSQL types correctly mapped (lines 364-381 in postgresql.ts)
  • ✅ MySQL types properly defined (lines 321-336 in mysql.ts)
  • ✅ SQLite types appropriately handled (lines 492-507 in sqlite.ts)
  • ✅ Maintains existing computed field logic in fieldRef() method
  • ✅ Affects both SELECT and WHERE clauses as intended

Test Coverage:

Technical Review

Type Mappings:

  • PostgreSQL: UUID → text, BigInt → bigint, etc. - all appropriate
  • MySQL: String → char, DateTime → datetime(3) - standard choices
  • SQLite: Proper type coercion (String → text, Boolean → integer)

Performance Considerations:

  • ✅ Minimal overhead - casting only applied to fields with @db.* annotations
  • ✅ No impact on regular fields without native type attributes
  • ✅ SQL-level casting is efficient

Security:

  • ✅ No SQL injection risks - uses parameterized casting via Kysely
  • ✅ Type safety maintained through abstract method pattern

Critical Issues Found ⚠️

I've identified several issues that align with CodeRabbit's findings:

  1. Array Type Handling (base-dialect.ts:1377): The casting logic doesn't preserve array types. For fields like String[] @db.Uuid, it casts to text instead of text[], which could break array operations.

  2. MySQL CAST Compatibility (mysql.ts:321-335): Some return values aren't valid MySQL CAST targets:

    • varchar(191) → should be CHAR(191)
    • tinyint(1) → should be UNSIGNED or SIGNED
    • bigint → should be SIGNED or UNSIGNED
    • longblob → should be BINARY
  3. Inconsistent Delegate Field Handling (expression-transformer.ts:914): The delegate base field selection was updated to use fieldRef(), which is good, but the implementation should be verified for consistency.

  4. Test Coverage Gap (issue-2394.test.ts:22): The test only checks that the query resolves, not that it returns the expected results. This could miss cases where casting makes comparisons always false.

Minor Observations

  1. Documentation: Consider adding JSDoc comments to getSqlType() method explaining the purpose and return values
  2. Edge Cases: The implementation correctly handles undefined return from getSqlType()
  3. Consistency: All three dialects follow the same pattern for enum handling (reducing to base text types)

Problem Solved

The fix addresses the core issue where PostgreSQL fields with @db.Uuid are stored as UUID type but compared against text values in policies, causing operator does not exist: uuid = text errors. By casting these fields back to their base SQL types, comparisons work correctly.

This is a targeted, low-risk fix that solves a specific database compatibility issue without affecting the broader system architecture.

Recommendation: MERGE with suggested fixes

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.

1 participant