Skip to content

Resolve pg bindings by structure, not instanceof, to survive bundle dupes - #969

Merged
wmadden merged 2 commits into
mainfrom
worktree/catalog-pn-boot-crash-f53963
Jul 13, 2026
Merged

Resolve pg bindings by structure, not instanceof, to survive bundle dupes#969
wmadden merged 2 commits into
mainfrom
worktree/catalog-pn-boot-crash-f53963

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Linked issue

n/a — surfaced from a reported catalog-service boot-crash-loop; no tracking issue exists yet.

At a glance

@prisma-next/postgres now identifies a caller-supplied pg handle by shape, not class identity:

export const isPgPool = (pg: Pool | Client): pg is Pool =>
  'totalCount' in pg && 'idleCount' in pg && 'waitingCount' in pg;

export const isPgClient = (pg: Pool | Client): pg is Client =>
  'escapeIdentifier' in pg && 'escapeLiteral' in pg;

Before, resolvePostgresBinding did pgBinding instanceof PgPool / instanceof PgClient. When an app bundler ships two copies of pg, the caller's Pool is an instance of a different Pool class than the one imported here, both checks fall through, and it throws Unable to determine pg binding type from pg input — crashing any bundled pnPostgres() service at boot, before any connection is attempted.

Summary

A bundle that duplicates pg (the classic phantom-dependency situation an app bundler creates) breaks the runtime's instanceof-based binding resolution, because instanceof compares against one specific module's Pool/Client class. This replaces the class-identity checks with structural predicates that hold across duplicate module copies, killing this class of boot crash regardless of how a downstream bundler resolves pg.

Decision

Two things ship:

  1. Structural binding resolution. resolvePostgresBinding (and the supabase toPool helper) identify Pool vs Client by disjoint structural members instead of instanceof. Pool has totalCount/idleCount/waitingCount; Client has escapeIdentifier/escapeLiteral. in-operator narrowing means zero new casts.
  2. Shared, exported predicates. isPgPool/isPgClient are exported from @prisma-next/postgres/runtime and reused by the supabase extension, so all three former instanceof sites share one definition rather than each carrying its own class-identity check.

The explicit escape hatch — binding: { kind: 'pgPool', pool } — is unchanged and remains the unambiguous override.

How it fits together

  1. Add the two predicates in binding.ts and drop the now-unused pg value import (the file keeps only the type-only import, so it no longer pulls pg's runtime into whatever bundles it).
  2. Swap the two instanceof branches in resolvePostgresBinding for isPgPool/isPgClient; the "neither Pool nor Client" throw is untouched.
  3. Export the predicates from exports/runtime.ts.
  4. Point the supabase toPool helper at the shared isPgPool in supabase.ts, guarding the optional options.pg for undefined first.

Behavior changes & evidence

  • A Pool (or Client) that is not an instanceof this package's pg copy now resolves correctly instead of throwing at boot — binding.ts. The regression test asserts the exact duplicate-copy condition: a duck-typed pool where pool instanceof Pool === false yet isPgPool(pool) === true, and resolvePostgresBinding returns { kind: 'pgPool', pool }postgres/test/binding.test.ts.
  • The supabase runtime accepts a cross-copy pool the same way — supabase.ts, covered through the public export in supabase/test/binding.test.ts.

Reviewer notes

  • The in-operator discriminators are disjoint on real pg (verified against pg 8.21.0): Pool has no escapeIdentifier; Client has no idleCount. Order of the two checks is therefore not significant.
  • Two existing vi.mock('pg', …) fixtures (postgres.test.ts, postgres-close.test.ts) had fake Pool/Client classes that only implemented connect/query/end. Once resolution became structural those fakes matched neither predicate, so I added the missing members. The edits are additive — no assertion changed — and the "throws for unrecognized pg input" test is untouched.
  • This is a prisma-next patch that is orthogonal to, and stronger than, fixing the phantom pg dependency on the bundler side: it survives any bundler that duplicates pg, not just the one that motivated it. The version bump / publish is a separate release step, not included here.

Testing performed

  • pnpm --filter @prisma-next/postgres test — 118 passed
  • pnpm --filter @prisma-next/extension-supabase test — 53 passed
  • pnpm --filter @prisma-next/postgres build (refreshes the /runtime export the supabase extension consumes)
  • pnpm --filter @prisma-next/postgres typecheck and pnpm --filter @prisma-next/extension-supabase typecheck — clean
  • pnpm lint:deps — no violations (1187 modules cruised)

Test-first: the duck-typed-pool assertions were written first and confirmed failing against the instanceof code before the fix.

Skill update

n/a — internal only. isPgPool/isPgClient are runtime type guards, not part of the CLI or authoring surface.

Alternatives considered

  • Explicit binding: { kind: 'pgPool', pool } at the call site (compose-side fix). Correct and supported, but it only fixes the one app that adopts it; every other pnPostgres() consumer stays exposed. The structural fix here removes the hazard for all of them.
  • Duck-type on connect + query. Insufficient: both Pool and Client expose both as functions, so it cannot tell them apart and would misroute a Client into the pool driver.
  • Keep instanceof with a structural fallback. More code for no benefit — the structural check subsumes the instanceof case and also removes the need for the pg value import.

Checklist

  • All commits are signed off (git commit -s) per the DCO.
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated.
  • The PR title is in TML-NNNN: form — no Linear ticket exists for this reported boot-crash; a descriptive title is used instead.
  • The Skill update section above is filled in.

Summary by CodeRabbit

  • New Features
    • Exposed PostgreSQL runtime helpers for detecting pool and client objects.
  • Bug Fixes
    • Improved PostgreSQL and Supabase handling by switching to compatibility-based detection rather than strict class identity, fixing scenarios with duplicated runtime copies.
  • Tests
    • Added unit coverage for pool/client detection (real and duck-typed objects).
    • Enhanced Postgres-related mocks with additional pool metrics and client escape helpers.
  • Documentation
    • Updated upgrade guidance to reflect the detection change and its compatibility impact.

@wmadden-electric
wmadden-electric requested a review from a team as a code owner July 13, 2026 13:54
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@wmadden-electric, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 7ded108c-72c0-4ac7-b461-d00a2fffe226

📥 Commits

Reviewing files that changed from the base of the PR and between c4fb0fc and 8e28ceb.

📒 Files selected for processing (8)
  • packages/3-extensions/postgres/src/exports/runtime.ts
  • packages/3-extensions/postgres/src/runtime/binding.ts
  • packages/3-extensions/postgres/test/binding.test.ts
  • packages/3-extensions/postgres/test/postgres-close.test.ts
  • packages/3-extensions/postgres/test/postgres.test.ts
  • packages/3-extensions/supabase/src/runtime/supabase.ts
  • packages/3-extensions/supabase/test/binding.test.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md
📝 Walkthrough

Walkthrough

PostgreSQL runtime exports structural pool/client guards and uses them for binding resolution. Supabase replaces constructor-based pool detection with the shared guard, while PostgreSQL mocks, tests, and upgrade instructions cover the updated behavior.

Changes

PostgreSQL binding detection

Layer / File(s) Summary
Structural binding guards and resolution
packages/3-extensions/postgres/src/runtime/binding.ts, packages/3-extensions/postgres/src/exports/runtime.ts, packages/3-extensions/postgres/test/binding.test.ts, packages/3-extensions/postgres/test/postgres*.test.ts, skills/extension-author/.../instructions.md
Exports isPgPool and isPgClient, uses them in resolvePostgresBinding, tests real and duck-typed pool/client objects, expands PostgreSQL mocks, and documents the upgrade behavior.
Supabase pool integration
packages/3-extensions/supabase/src/runtime/supabase.ts, packages/3-extensions/supabase/test/binding.test.ts
Supabase uses isPgPool instead of instanceof Pool and tests detection of a duck-typed pool.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. 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 accurately summarizes the main change: switching pg binding detection from instanceof to structural checks to handle duplicate bundled copies.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree/catalog-pn-boot-crash-f53963

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.

@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@969

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@969

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@969

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@969

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@969

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@969

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@969

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@969

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@969

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@969

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@969

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@969

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@969

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@969

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@969

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@969

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@969

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@969

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@969

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@969

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@969

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@969

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@969

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@969

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@969

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@969

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@969

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@969

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@969

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@969

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@969

@prisma-next/config-loader

npm i https://pkg.pr.new/@prisma-next/config-loader@969

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@969

@prisma-next/language-server

npm i https://pkg.pr.new/@prisma-next/language-server@969

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@969

prisma-next

npm i https://pkg.pr.new/prisma-next@969

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@969

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@969

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@969

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@969

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@969

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@969

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@969

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@969

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@969

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@969

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@969

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@969

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@969

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@969

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@969

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@969

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@969

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@969

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@969

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@969

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@969

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@969

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@969

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@969

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@969

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@969

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@969

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@969

commit: 8e28ceb

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 155.2 KB (+0.03% 🔺)
postgres / emit 130.22 KB (+0.04% 🔺)
mongo / no-emit 98.7 KB (0%)
mongo / emit 89.43 KB (0%)
cf-worker / no-emit 182.7 KB (0%)
cf-worker / emit 155.2 KB (0%)

@wmadden
wmadden enabled auto-merge July 13, 2026 15:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md`:
- Around line 590-596: Revise the paragraph describing Pool/Client resolution to
state that shape-based guards preserve existing same-copy behavior while
additionally accepting structurally compatible cross-copy or duck-typed handles.
Replace the universal “at boot” failure wording with the specific
binding-resolution paths, including connect() and toPool(), and retain the
statement that no extension-author action is required.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 37843398-d6c2-4bae-9c35-b6d3be99b5a3

📥 Commits

Reviewing files that changed from the base of the PR and between 10ad7f1 and c4fb0fc.

📒 Files selected for processing (1)
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md

wmadden-electric and others added 2 commits July 13, 2026 18:00
…upes

resolvePostgresBinding and the supabase toPool helper identified pg
Pool/Client handles with instanceof. When an app bundler produces two
copies of pg (a phantom-dependency duplication), the caller`s Pool is an
instance of a different Pool class than the one imported here, so the
instanceof checks fall through and resolvePostgresBinding throws "Unable
to determine pg binding type from pg input" — crashing any bundled
pnPostgres() service at boot before any connection is attempted.

Replace the three instanceof checks with cast-free structural predicates
isPgPool/isPgClient (in-operator narrowing on disjoint members: Pool has
totalCount/idleCount/waitingCount, Client has escapeIdentifier/
escapeLiteral). These are structural, so they hold across duplicate pg
module copies. The predicates are exported from
@prisma-next/postgres/runtime and reused by the supabase extension.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
The pg binding fix touches packages/3-extensions/ (postgres + supabase), so
the per-pr-declaration upgrade-coverage gate requires a declaration in the
in-flight 0.14-to-0.15 instructions. The change is additive plus a bug fix
with no extension-author action, so record it as an incidental substrate diff.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric force-pushed the worktree/catalog-pn-boot-crash-f53963 branch from c4fb0fc to 8e28ceb Compare July 13, 2026 16:02
@wmadden
wmadden added this pull request to the merge queue Jul 13, 2026
Merged via the queue into main with commit 563a94c Jul 13, 2026
21 checks passed
@wmadden
wmadden deleted the worktree/catalog-pn-boot-crash-f53963 branch July 13, 2026 16:29
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.

2 participants