Skip to content

fix(target-postgres): decode native-enum array columns - #30195

Open
StevenMcClankerton wants to merge 2 commits into
mainfrom
issue-30164
Open

fix(target-postgres): decode native-enum array columns#30195
StevenMcClankerton wants to merge 2 commits into
mainfrom
issue-30164

Conversation

@StevenMcClankerton

@StevenMcClankerton StevenMcClankerton commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

An enum-array column (pg.enum(Mood)[]) could not be read: the array-of-enum OID is allocated per-database, pg-types has no parser for it, the value arrived as the raw literal {URGENT,LOW}, and decoding crashed with TypeError: Cannot read properties of undefined (reading 'codecId').

Fixes #30164.

This PR contains two independent changes.

Change A — a framework-wide this-binding bug

materializeCodec called blindCast<…>(descriptor.factory)(validated)(ctx), detaching factory from its receiver. Any descriptor whose factory builds its codec via new XCodec(this) (the standard pattern) therefore produced a codec with descriptor === undefined.

The corrected causal chain is stronger than the issue's own guess: the codec was found; its id getter crashed. CodecImpl.get id() is this.descriptor.codecId (codec.ts:73); decoding.ts:250-259 calls wrapDecodeFailure, which reads codec.id at :168. encoding.ts:131,150 had the identical latent break on the encode side. This is why the error named no column — the good message (Failed to decode column ${table}.${column}…) already existed in wrapDecodeFailure, it just crashed before it could render.

Blast radius is small: this.descriptor is dereferenced at exactly four production sites, none branching or memoising on it. Closure-style factories were already unaffected — PostgresCodecDescriptorAdapter assigns this.factory = (params) => descriptor.factory(params) (codec-descriptor.ts:139), an arrow that already forwards correctly, so every adapted extension descriptor (pgvector, postgis, arktype-json) was already fine on main; only directly-declared CodecDescriptorImpl subclasses were broken. Removing the blindCast is a strict narrowing.

Change B — cast enum-array projections to ::text[]

projectsNativeEnumArray gates renderProjection and renderReturning on codec.codecId === pgEnumDescriptor.codecId && codec.many === true — facts read from the contract, never from the wire value — and appends ::text[] to the rendered column. That puts the column on OID 1009, which pg-types already parses into a string array; pg/enum@1's decode is a passthrough.

Why this layer. Array-literal parsing does not belong in sql-runtime/decoding.ts — that package is target-agnostic (no-target-branches), and the postgres target/adapter package is the right home for a postgres wire-format fact.

Rejected approaches:

  • A wire-value heuristic keyed on "pg-types has no parser for this OID" is unsafe: that bucket also contains text (OID 25) and varchar (1043). Verified end-to-end against real Postgres, it corrupted a text column holding '{"a": 1}' into ["a", ": 1"] and threw array dimension not balanced on unbalanced braces — reintroducing exactly the "no column named" diagnostic problem this issue complains about, on a new path.
  • Per-connection pg_type OID discovery (querying typarray for native enum types once per connection) works but adds a bootstrap query that shows up in every exact-query-sequence test in the driver suite (driver.pinned-client-serialization.test.ts, driver.stream-portal-protection.integration.test.ts, driver.prepared.test.ts).

The projection cast needs neither: no wire probe, no unregistered-OID guessing, and it never touches text columns.

Completeness. renderProjection has one caller (renderSelect), which serves top-level, derived-table, and subquery SELECTs at every nesting depth; renderReturning covers INSERT/UPDATE/DELETE. include relations never emit a raw enum array at all — they route through jsonArrayProjection (codec-descriptor.ts:51-82), which unnests and re-aggregates as json/jsonb (OIDs 114/3802), so nested/aggregated reads were never affected by this bug.

Two shapes are unreachable today but would reintroduce the bug if these surfaces ever grow computed items: a literal-expression projection short-circuits before the cast check, and a non-column-ref RETURNING item skips it. Worth a follow-up if either surface starts accepting computed enum-array expressions.

The forced explicit alias in renderReturning when the cast fires is defensive, not load-bearing — Postgres already preserves the column name through a cast in its own column-naming — chosen so the emitted SQL is self-describing.

A second, pre-existing reproduction, un-masked

ports/engines/queries/filters/field-reference/enum-filter (enum_filter.test.ts) was marked it.fails in #29924 without anyone diagnosing why. It creates a native-enum array column (enum2: ['a','b']) and hit this exact decode bug. It is now it and green.

While un-skipping it, I removed a duplicate assertion: two expect(...) calls in that test invoked the identical referencedScalarInList(scalar, list, true) under two different labels ('notIn' and 'not: { in }'), with no way, given the helper's negated: boolean signature, to construct a genuinely different negation form. The same copy-paste pair exists verbatim in eight sibling filter ports (bytes_filter, datetime_filter, decimal_filter, bigint_filter, string_filter, int_filter, float_filter, plus enum_filter itself). This PR only cleans enum_filter, ahead of it going green; a sweep of the other eight should be its own follow-up ticket.

Testing

  • Full test/integration suite exits zero: 372 files / 2063 passed | 52 expected fail (2115 total).
  • sql-renderer.enum-array-projection-cast.test.ts pins exact rendered SQL for the enum-array SELECT and RETURNING cast, plus four negatives (scalar enum, ordinary text[], scalar text, no codec) so a future widening of the guard breaks a test instead of silently casting the wrong column.
  • The issues-30164-enum-array-decode port fixture asserts through the full ORM stack that a plain note: '{"a": 1}' column round-trips as a string beside a correctly-decoded moods array, on both the create() RETURNING path and a plain read.
  • materialize-codec.test.ts pins Change A directly: a CodecImpl subclass whose descriptor factory does new XCodec(this), resolved through materializeCodec, has a working .id.
  • driver-postgres returns to its untouched 151/151 baseline; temporal-text-parsers.ts is unchanged from main, and the three exact-query-sequence tests it has were never touched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UcqoY3CKfnubdZt5YQk2Rq

Summary by CodeRabbit

  • Bug Fixes
    • Fixed codec materialization so descriptor-based codecs preserve their expected behavior during encoding and decoding.
    • Corrected PostgreSQL projections and RETURNING clauses for native enum arrays by applying the appropriate text-array conversion while preserving aliases.
    • Fixed enum field-reference inclusion filters so they pass as expected.
    • Added support coverage for decoding native PostgreSQL enum arrays during record creation and retrieval.

@StevenMcClankerton
StevenMcClankerton requested a review from a team as a code owner September 1, 2026 18:54
@pkg-pr-new

pkg-pr-new Bot commented Sep 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma/orm-extension-arktype-json

npm i https://pkg.pr.new/@prisma/orm-extension-arktype-json@30195

@prisma/orm-extension-middleware-cache

npm i https://pkg.pr.new/@prisma/orm-extension-middleware-cache@30195

@prisma/orm-extension-paradedb

npm i https://pkg.pr.new/@prisma/orm-extension-paradedb@30195

@prisma/orm-extension-pgvector

npm i https://pkg.pr.new/@prisma/orm-extension-pgvector@30195

@prisma/orm-extension-postgis

npm i https://pkg.pr.new/@prisma/orm-extension-postgis@30195

@prisma/orm-extension-supabase

npm i https://pkg.pr.new/@prisma/orm-extension-supabase@30195

@prisma/orm-family-mongo

npm i https://pkg.pr.new/@prisma/orm-family-mongo@30195

@prisma/orm-family-sql

npm i https://pkg.pr.new/@prisma/orm-family-sql@30195

@prisma/orm-framework

npm i https://pkg.pr.new/@prisma/orm-framework@30195

@prisma/orm-mongo

npm i https://pkg.pr.new/@prisma/orm-mongo@30195

@prisma/orm-postgres

npm i https://pkg.pr.new/@prisma/orm-postgres@30195

@prisma/orm-sqlite

npm i https://pkg.pr.new/@prisma/orm-sqlite@30195

@prisma/orm-target-mongo

npm i https://pkg.pr.new/@prisma/orm-target-mongo@30195

@prisma/orm-target-postgres

npm i https://pkg.pr.new/@prisma/orm-target-postgres@30195

@prisma/orm-target-sqlite

npm i https://pkg.pr.new/@prisma/orm-target-sqlite@30195

@prisma/orm-toolchain

npm i https://pkg.pr.new/@prisma/orm-toolchain@30195

commit: 2e145d7

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
postgres / no-emit 174.96 KB (+0.02% 🔺)
postgres / emit 152.17 KB (+0.04% 🔺)
mongo / no-emit 101.09 KB (-0.01% 🔽)
mongo / emit 90.95 KB (0%)
cf-worker / no-emit 198.96 KB (+0.04% 🔺)
cf-worker / emit 173.47 KB (+0.04% 🔺)

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Team

Run ID: 27fba84c-0fa3-47b6-9013-a5a915542e01

📥 Commits

Reviewing files that changed from the base of the PR and between dd846dc and 2e145d7.

⛔ Files ignored due to path filters (2)
  • test/integration/test/enum-array-decode/_fixture/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/enum-array-decode/_fixture/generated/contract.json is excluded by !**/generated/**
📒 Files selected for processing (8)
  • packages/1-framework/1-core/framework-components/src/shared/resolve-codec.ts
  • packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
  • packages/3-targets/6-adapters/postgres/test/sql-renderer.enum-array-projection-cast.test.ts
  • test/integration/test/enum-array-decode/_fixture/contract.prisma
  • test/integration/test/enum-array-decode/_fixture/prisma.config.ts
  • test/integration/test/enum-array-decode/enum-array-decode.test.ts
  • test/integration/test/ports/engines/queries/filters/field_reference/enum_filter/enum_filter.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/3-targets/6-adapters/postgres/test/sql-renderer.enum-array-projection-cast.test.ts
  • packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
  • test/integration/test/ports/engines/queries/filters/field_reference/enum_filter/enum_filter.test.ts
  • packages/1-framework/1-core/framework-components/src/shared/resolve-codec.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The change preserves descriptor context during codec materialization and casts PostgreSQL native enum-array projections to text[]. Tests cover codec resolution, SQL rendering, enum-array decoding, and enum filters.

Changes

Enum-array codec flow

Layer / File(s) Summary
Descriptor-bound codec materialization
packages/1-framework/1-core/framework-components/src/shared/resolve-codec.ts, packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts
materializeCodec invokes descriptor.factory as a descriptor method. Tests cover parameterized and non-parameterized codecs and verify encode/decode behavior.
PostgreSQL enum-array projection casts
packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts, packages/3-targets/6-adapters/postgres/test/sql-renderer.enum-array-projection-cast.test.ts
The renderer detects native enum arrays and adds ::text[] casts to SELECT and RETURNING expressions. Tests verify aliases and unaffected scalar or text projections.
Enum integration regression coverage
test/integration/test/enum-array-decode/..., test/integration/test/ports/engines/queries/filters/field_reference/enum_filter/enum_filter.test.ts
A native enum-array fixture and integration test verify creation and reads. The enum inclusion filter now runs as a passing test.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 2e145

The change adds native enum-array decoding support and fixes codec materialization behavior, with targeted and full-suite tests reported passing; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: aqrln

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 9 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: fixing decoding of PostgreSQL native-enum array columns.
Linked Issues check ✅ Passed The changes address issue #30164 by casting native enum-array projections and RETURNING expressions to ::text[], preserving scalar enum behavior, fixing descriptor-bound codec materialization, and add…
Out of Scope Changes check ✅ Passed The changes remain within scope. The codec regression test, SQL-renderer tests, integration fixture, enum-array read test, and re-enabled enum filter test support the native enum-array decoding fix an…
Full details: Linked Issues check

Explanation

The changes address issue #30164 by casting native enum-array projections and RETURNING expressions to ::text[], preserving scalar enum behavior, fixing descriptor-bound codec materialization, and adding end-to-end read coverage. The generated fixture files were excluded by the !/generated/ filter, but their absence does not prevent assessment of the reviewed implementation.

Full details: Out of Scope Changes check

Explanation

The changes remain within scope. The codec regression test, SQL-renderer tests, integration fixture, enum-array read test, and re-enabled enum filter test support the native enum-array decoding fix and its regression coverage.

Full details: Docstring Coverage

Explanation

Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 9 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-30164

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

SevInf and others added 2 commits September 2, 2026 13:20
A pg.enum(Mood)[] column could not be read: the array-of-enum OID is
allocated per-database, pg-types has no parser for it, and the wire
value arrived as the raw literal `{URGENT,LOW}`, so decoding crashed
before it could report which column failed.

Two independent fixes:

- materializeCodec called descriptor.factory detached from its
  receiver (`blindCast<...>(descriptor.factory)(validated)(ctx)`), so
  any descriptor whose factory builds its codec via `new XCodec(this)`
  produced a codec with `descriptor === undefined`. That codec was
  found correctly; only its `id` getter crashed inside
  wrapDecodeFailure/wrapEncodeFailure, which is why the error named no
  column. Calling factory as a method on descriptor fixes both paths.

- renderProjection/renderReturning now cast a many + pg/enum@1
  projection to `::text[]`, putting the column on OID 1009 which
  pg-types already parses into a string array. This is a target-owned
  fact read from the contract, not a wire-value heuristic, and keeps
  array-literal parsing out of the target-agnostic sql-runtime decoder.

Un-skips ports/engines/queries/filters/field-reference/enum-filter,
which was marked it.fails in #29924 without diagnosis: it was this
same decode bug via a native-enum array column.

Fixes #30164.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UcqoY3CKfnubdZt5YQk2Rq
Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
The suite is not a port of an upstream Prisma test, so it moves to its
own directory alongside the relocated harness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UcqoY3CKfnubdZt5YQk2Rq
Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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.

Enum-array columns cannot be decoded: codec lookup returns undefined (reading 'codecId')

2 participants