fix(sql-runtime): verify contract marker before BEGIN - #30332
Draft
kristof-siket wants to merge 1 commit into
Draft
kristof-siket wants to merge 1 commit into
kristof-siket wants to merge 1 commit into
Conversation
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>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
@prisma/orm-extension-arktype-json
@prisma/orm-extension-middleware-cache
@prisma/orm-extension-paradedb
@prisma/orm-extension-pgvector
@prisma/orm-extension-postgis
@prisma/orm-extension-supabase
@prisma/orm-family-mongo
@prisma/orm-family-sql
@prisma/orm-framework
@prisma/orm-mongo
@prisma/orm-postgres
@prisma/orm-sqlite
@prisma/orm-target-mongo
@prisma/orm-target-postgres
@prisma/orm-target-sqlite
@prisma/orm-toolchain
commit: |
Contributor
size-limit report 📦
|
5 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
BEGINand the user's first statement.The defect
withTransaction(runtime, fn)callsruntime.connection()and thenconnection.transaction(), which sendsBEGIN. Every statement first awaits the single-flight marker gate (setupDriverExecution→verifyMarker), andverifyMarkerreads the marker throughthis.driver. On a fresh runtime the first statement of the first transaction therefore triggers the marker read afterBEGIN.What that read does depends on the 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).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 isSET TRANSACTION ISOLATION LEVEL SERIALIZABLE. It was first seen on8.0.0-rc.8; the rows below are frommain(1c434a7555) and from this branch:mainthrew 25001 | ok | okok | ok | okok | okok | okThe pooled variant was already known to the test suite:
test/e2e/framework/test/transaction-orm.test.tsran 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 onmainwithSqlConnectionError: Connection terminated due to connection timeout.The fix
SqlRuntimeBase.acquireRawConnection()awaits the existing marker gate before it callsdriver.acquireConnection(), andconnection()acquires through it. Every path that can begin a transaction goes through one of the two (withTransaction, the ORM mutation executor'sconnection().transaction(), the Supabase role session), so the marker read always finishes before a connection is held and beforeBEGIN.The gate itself is unchanged: one read per runtime, shared by concurrent first operations, and no read when
verifyMarkerisfalse. No second marker read was added. No public API changed.Testing performed
Each new test was run against
mainfirst and failed there.packages/2-sql/5-runtime/test/marker-verification.test.ts— newverifyMarker and transactionsblock: 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: falsereads nothing; a failed marker read acquires no connection. Onmain:2 failed | 10 passed (12).packages/3-extensions/supabase/test/supabase-runtime.test.ts— the role session transaction reads the marker beforeBEGIN. Onmain: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 SERIALIZABLEas the first statement, thenSHOW transaction_isolationreturnsserializable. Onmain: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. Onmainwithout it: 5 failed with the connection timeout above. With the fix: 5 passed.Suites run on the final commit:
pnpm typecheck— exit 0pnpm 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 touchespackages/3-extensions/only in a test file, so no upgrade fragment is required.mainand 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
git commit -s) per the DCO. The DCO status check will block merge if any commit is missing aSigned-off-by:trailer.n/aif the change is doc-only / refactor with no behavioural delta).TML-NNNN: <sentence-case title>form (Linear ticket prefix + concise title naming the concrete deliverable). See.claude/skills/create-pr/SKILL.mdfor 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.n/a — internal only).Notes for the reviewer
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 beforeacquireConnection()covers both and needs no change in the Supabase runtime, which begins its own transactions on a connection fromacquireRawConnection().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 fromconnection()instead of from the first statement.docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md(transaction lifecycle) describes the ordering.isolationLeveloption tobeginTransaction/transaction/withTransaction/db.transaction, so users no longer hand-writeSET TRANSACTION.