feat(sql-runtime): transaction isolation level option - #30333
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
Users who need REPEATABLE READ or SERIALIZABLE had to hand-write
SET TRANSACTION ISOLATION LEVEL as the first statement of a transaction,
because no layer took an isolation level.
SqlConnection.beginTransaction, RuntimeConnection.transaction,
withTransaction, and the Postgres and Supabase db.transaction take an
optional { isolationLevel }. The Postgres driver sends
BEGIN ISOLATION LEVEL <LEVEL> as one statement, and plain BEGIN when no
level is given. The SQLite driver rejects a requested level with
DRIVER.ISOLATION_LEVEL_UNSUPPORTED, because every SQLite transaction is
serializable; the SQLite client keeps its one-argument transaction(fn).
withTransaction releases the connection when the transaction cannot
begin, so a rejected level does not leave a connection checked out.
The change is additive: every new parameter is optional and existing
zero-argument implementations still type-check.
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 📦
|
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 — no issue exists yet. CONTRIBUTING.md asks for an issue before a feature, and the runtime subsystem doc lists "default isolation level only" as a deferred non-goal, so this is a draft to get direction-fit feedback on a concrete shape. It is stacked on #30332 (the marker-before-
BEGINfix) and targets that branch.Summary
Users who need REPEATABLE READ or SERIALIZABLE have to hand-write
SET TRANSACTION ISOLATION LEVEL ...as the first statement of a transaction, because no layer takes an isolation level. That rawSETis what exposed the bug fixed in #30332, and it stays fragile: it only works while nothing else runs first in the transaction. This PR adds anisolationLeveloption from the driver interface up todb.transaction, and the Postgres driver applies it in theBEGINstatement itself.The change is additive and non-breaking. Every new parameter is optional, omitting it sends the same plain
BEGINas before, and a driver orRuntimeConnectionwritten against the old zero-argument signatures still type-checks.API added
New error code:
DRIVER.ISOLATION_LEVEL_UNSUPPORTED(payloadtarget,isolationLevel).Behaviour per target
BEGIN ISOLATION LEVEL <LEVEL>as one statement when a level is given, plainBEGINotherwise. The level name comes from a fixed map looked up withObject.hasOwn; an unknown value from an untyped caller rejects withDRIVER.ISOLATION_LEVEL_UNSUPPORTEDand sends nothing.DRIVER.ISOLATION_LEVEL_UNSUPPORTEDbeforeBEGIN. Every SQLite transaction is serializable, so there is no level to select. The SQLite client'sdb.transaction(fn)keeps its one-argument signature, so asking for a level there is a compile error; the runtime error coverswithTransaction(runtime, fn, options).transaction(options)andRoleBoundDb.transaction(fn, options)forward to the Postgres driver.SqlConnectionorRuntimeConnection.withTransactionnow releases the connection and skips the callback whenconnection.transaction(options)rejects. Before this PR a failedBEGINleft the connection checked out; an unsupported level makes that path easy to reach, so it is fixed here.Testing performed
New tests:
packages/3-targets/7-drivers/postgres/test/driver.isolation-level.integration.test.ts(real PGlite database): each of the four levels is applied on the single-client driver (show transaction_isolationinside the transaction);serializableis applied on the pooled driver; no level keepsread committed, including after a serializable transaction on the same connection;BEGINis one statement (BEGIN,BEGIN ISOLATION LEVEL REPEATABLE READ); an unknown level rejects and sends nothing.packages/3-targets/7-drivers/sqlite/test/sqlite-driver.test.ts: a requested level rejects with the structured error, no transaction is begun, and the connection still works.packages/2-sql/5-runtime/test/sql-runtime.test.ts: options reach the driver fromwithTransactionand fromconnection.transaction(); no options meansbeginTransaction(undefined); a failed begin releases the connection and skips the callback.packages/3-extensions/postgres/test/postgres.test.tsandtransaction.types.test-d.ts:db.transaction(fn, { isolationLevel })sendsBEGIN ISOLATION LEVEL SERIALIZABLE; the option is typed, and an unknown level is a type error.packages/3-extensions/sqlite/test/transaction.types.test-d.ts: the SQLite client rejects the option at compile time.packages/3-extensions/supabase/test/supabase-runtime.test.ts: the role session forwards the option.packages/2-sql/4-lanes/relational-core/test/ast/driver-types.types.test-d.ts: the options type, and that a zero-argumentbeginTransactionstill satisfiesSqlConnection.test/e2e/framework/test/transaction.test.ts(fresh runtime,withTransactionwithserializable, then defaultread committed),transaction-orm.test.ts(db.transaction(fn, { isolationLevel: 'repeatableRead' })with an ORM write inside),sqlite/transaction.test.ts(rejects, database stays usable).Suites run on the final commit:
pnpm build—Tasks: 86 successful, 86 totalpnpm typecheck— exit 0 (Tasks: 169 successful, 169 total)pnpm lint— exit 0 (Tasks: 101 successful, 101 total)pnpm lint:deps— exit 0 (no dependency violations found)pnpm test:packages—Test Files 1287 passed | 1 skipped (1288),Tests 17272 passed | 3 expected fail | 1 skipped (17276)pnpm test:e2e—Test Files 22 passed (22),Tests 123 passed (123)pnpm test:integration—Test Files 395 passed (395),Tests 2161 passed | 52 expected fail (2213)pnpm check:error-reference—error-reference lists all 321 known codes.pnpm lint:skills—All skills passed validation (skills-contrib, skills).pnpm check:upgrade-coverage --mode pr --prev <#30332 head> --head HEAD— exit 0record-upgrade-instructions: restoredpackages/3-extensions/to the fix(sql-runtime): verify contract marker before BEGIN #30332 head, applied the fragment,git status --porcelainoutside test directories printed nothing, test directories stayed at base, andpnpm test --filter='./packages/3-extensions/*'passed (Tasks: 67 successful, 67 total).Skill update
skills/prisma-8/references/queries-postgres.md(Workflow — Transactions) teachesdb.transaction(fn, { isolationLevel }), tells the agent not to hand-writeSET TRANSACTION, and notes that serialization failures (SQLSTATE40001) are not retried.references/runtime.mdstates the Postgres / SQLite difference andreferences/supabase.mdtheRoleBoundDb.transaction(fn, options?)signature.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
docs/architecture docs/subsystems/4. Runtime & Middleware Framework.mdlisted "default isolation level only" as deferred, andscorecard/14-transactions.mdmarkedisolationLevelas not in 8.0. Both are updated here. If the deferral should stand for 8.0, close this PR; fix(sql-runtime): verify contract marker before BEGIN #30332 does not depend on it.BEGIN, known only at run time, and the driver is the layer that owns that statement. A driver-side structured error keeps the runtime free of target branches.'serializable'too, although that is the level SQLite always gives. Accepting one value and rejecting three would make the option look supported; the error message says the transaction is already serializable and to remove the option.'noAction' | 'setNull','onFirstUse'), mapped to the PostgreSQL keywords in the driver.40001,READ ONLY/DEFERRABLE, client-level default options, timeouts. The options object leaves room for them.upgrade-instructions/pending/transaction-isolation-level/extension/— extension authors who implementSqlConnectionor build their ownRuntimeConnectionforward the new argument. Noappfragment:examples/is unchanged and app code needs no edit.docs/reference/error-reference.md, the scorecard row, and the sql-runtime / Postgres driver / SQLite driver READMEs.