Skip to content

fix(sql-runtime): verify contract marker before BEGIN - #30332

Draft
kristof-siket wants to merge 1 commit into
mainfrom
transaction-marker-before-begin
Draft

kristof-siket wants to merge 1 commit into
mainfrom
transaction-marker-before-begin

Conversation

@kristof-siket

Copy link
Copy Markdown
Contributor

Linked issue

n/a — small bug fix. No issue was opened; this draft carries the reproduction.

Summary

A fresh SQL runtime whose first operation is a transaction read the contract marker inside that transaction. The runtime now resolves the marker gate before it acquires the connection, so nothing runs between BEGIN and the user's first statement.

The defect

withTransaction(runtime, fn) calls runtime.connection() and then connection.transaction(), which sends BEGIN. Every statement first awaits the single-flight marker gate (setupDriverExecution → verifyMarker), and verifyMarker reads the marker through this.driver. On a fresh runtime the first statement of the first transaction therefore triggers the marker read after BEGIN.

What that read does depends on the driver:

  • Single-connection driver (pg: client, the serverless setup). The driver and the transaction share one socket, so the marker read is the transaction's first statement.
    • SET TRANSACTION ISOLATION LEVEL ... as the user's first statement fails with SQLSTATE 25001 (SET TRANSACTION ISOLATION LEVEL must be called before any query).
    • A REPEATABLE READ or SERIALIZABLE transaction takes its snapshot at the marker read, not at the user's first statement.
    • A marker read that fails aborts the user's transaction.
  • Pooled driver. The marker read asks the pool for a second client while the transaction holds the first. On a database that serves one connection at a time (PGlite, a pool with max: 1) the read never gets a client and the transaction times out.

Reproduction against PostgreSQL 18.3 with the single-connection driver (pg: client). Each transaction's first statement is SET TRANSACTION ISOLATION LEVEL SERIALIZABLE. It was first seen on 8.0.0-rc.8; the rows below are from main (1c434a7555) and from this branch:

Runtime state Transactions in a row main This branch
Fresh runtime 3 threw 25001 | ok | ok ok | ok | ok
Runtime that ran one plain query first 2 ok | ok ok | ok

The pooled variant was already known to the test suite: test/e2e/framework/test/transaction-orm.test.ts ran a warm-up query before its first transaction, with a comment that marker verification inside a transaction deadlocks on PGlite. Without the warm-up, all five tests in that file fail on main with SqlConnectionError: Connection terminated due to connection timeout.

The fix

SqlRuntimeBase.acquireRawConnection() awaits the existing marker gate before it calls driver.acquireConnection(), and connection() acquires through it. Every path that can begin a transaction goes through one of the two (withTransaction, the ORM mutation executor's connection().transaction(), the Supabase role session), so the marker read always finishes before a connection is held and before BEGIN.

The gate itself is unchanged: one read per runtime, shared by concurrent first operations, and no read when verifyMarker is false. No second marker read was added. No public API changed.

Testing performed

Each new test was run against main first and failed there.

  • packages/2-sql/5-runtime/test/marker-verification.test.ts — new verifyMarker and transactions block: the marker is read before the connection is acquired when a transaction is the first operation; it is read once across transactions and plain queries; verifyMarker: false reads nothing; a failed marker read acquires no connection. On main: 2 failed | 10 passed (12).
  • packages/3-extensions/supabase/test/supabase-runtime.test.ts — the role session transaction reads the marker before BEGIN. On main: expected [ 'begin', 'marker' ] to deeply equal [ 'marker', 'begin' ].
  • test/e2e/framework/test/transaction.test.ts — the 25001 reproduction on a fresh single-connection runtime: SET TRANSACTION ISOLATION LEVEL SERIALIZABLE as the first statement, then SHOW transaction_isolation returns serializable. On main: SqlQueryError: SET TRANSACTION ISOLATION LEVEL must be called before any query (code: '25001').
  • test/e2e/framework/test/transaction-orm.test.ts — the warm-up query is removed. On main without it: 5 failed with the connection timeout above. With the fix: 5 passed.

Suites run on the final commit:

  • pnpm typecheck — exit 0
  • pnpm lint — exit 0 (Tasks: 101 successful, 101 total)
  • pnpm lint:deps — exit 0 (no dependency violations found)
  • pnpm test:packages — Test Files 1286 passed | 1 skipped (1287), Tests 17260 passed | 3 expected fail | 1 skipped (17264)
  • pnpm test:e2e — Test Files 22 passed (22), Tests 120 passed (120)
  • pnpm test:integration — Test Files 395 passed (395), Tests 2161 passed | 52 expected fail (2213)
  • pnpm check:upgrade-coverage --mode pr --prev origin/main --head HEAD — exit 0. The diff touches packages/3-extensions/ only in a test file, so no upgrade fragment is required.
  • Manual: the reproduction table above, run against a local PostgreSQL 18.3 on main and on this branch.

Skill update

n/a — no user-facing surface changes. The fix removes a failure; no skill documents the old ordering or a workaround for it.

Checklist

  • All commits are signed off (git commit -s) per the DCO. The DCO status check will block merge if any commit is missing a Signed-off-by: trailer.
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated (or n/a if the change is doc-only / refactor with no behavioural delta).
  • The PR title is in TML-NNNN: <sentence-case title> form (Linear ticket prefix + concise title naming the concrete deliverable). See .claude/skills/create-pr/SKILL.md for the full convention. — No Linear ticket exists for this change, so the title uses the conventional-commit form from CONTRIBUTING.md. Happy to retitle once a ticket exists.
  • The Skill update section above is filled in (or stated n/a — internal only).

Notes for the reviewer

  • Where the gate sits. Awaiting the gate inside RuntimeConnection.transaction() also fixes the 25001 case, but the connection is already held at that point, so a pool with a single client would still wait on itself for the marker read. Awaiting it before acquireConnection() covers both and needs no change in the Supabase runtime, which begins its own transactions on a connection from acquireRawConnection().
  • Behaviour change to be aware of. runtime.connection() now performs the marker read on a fresh runtime even if the caller never runs a statement on the connection. A marker read that rejects surfaces from connection() instead of from the first statement.
  • Docs. docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md (transaction lifecycle) describes the ordering.
  • Follow-up. A stacked draft adds an isolationLevel option to beginTransaction / transaction / withTransaction / db.transaction, so users no longer hand-write SET TRANSACTION.

A fresh runtime whose first operation was a transaction read the contract
marker after BEGIN. On a single-connection driver that read was the first
statement of the transaction: SET TRANSACTION failed with SQLSTATE 25001,
REPEATABLE READ and SERIALIZABLE snapshots were taken at the marker read,
and a failed marker read aborted the transaction. On a pooled driver the
read waited for a second client, which never arrives when the database
serves one connection at a time.

The runtime now awaits the single-flight marker gate before it acquires a
connection, in acquireRawConnection(), and connection() acquires through
it. The gate is unchanged: one read per runtime, none when verifyMarker is
false.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Kristof Siket <siket@prisma.io>
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

@pkg-pr-new

pkg-pr-new Bot commented Sep 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma/orm-extension-arktype-json

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

@prisma/orm-extension-middleware-cache

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

@prisma/orm-extension-paradedb

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

@prisma/orm-extension-pgvector

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

@prisma/orm-extension-postgis

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

@prisma/orm-extension-supabase

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

@prisma/orm-family-mongo

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

@prisma/orm-family-sql

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

@prisma/orm-framework

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

@prisma/orm-mongo

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

@prisma/orm-postgres

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

@prisma/orm-sqlite

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

@prisma/orm-target-mongo

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

@prisma/orm-target-postgres

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

@prisma/orm-target-sqlite

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

@prisma/orm-toolchain

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

commit: a8af255

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
postgres / no-emit 189.33 KB (+0.02% 🔺)
postgres / emit 159.87 KB (+0.02% 🔺)
mongo / no-emit 109.31 KB (0%)
mongo / emit 91.8 KB (0%)
cf-worker / no-emit 212.38 KB (+0.01% 🔺)
cf-worker / emit 179.9 KB (+0.02% 🔺)

This branch has not been deployed

No deployments
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