Skip to content

feat(sql-runtime): add callback-based transaction API#346

Open
aqrln wants to merge 1 commit intotransaction-api-specfrom
transaction-api-m1
Open

feat(sql-runtime): add callback-based transaction API#346
aqrln wants to merge 1 commit intotransaction-api-specfrom
transaction-api-m1

Conversation

@aqrln
Copy link
Copy Markdown
Member

@aqrln aqrln commented Apr 15, 2026

PR Stack: Transaction API

PR Description Base
#345 📄 Spec, plan, and ADR main
#346 (this) ⚙️ Core transaction runtime + db.transaction() + tx.sql #345
#347 🔗 Wire tx.orm into transactions #346

Part of https://linear.app/prisma-company/issue/TML-1912/orm-transaction-support


Summary

Implements Milestone 1 of the transaction API: the core withTransaction helper in sql-runtime (target-agnostic) and the db.transaction() surface on PostgresClient with tx.sql support.

Implementation

withTransaction(runtime, fn)@prisma-next/sql-runtime

A target-agnostic helper that manages the full transaction lifecycle:

  1. Acquires a connection from the pool → starts a transaction
  2. Runs the user callback with a guarded TransactionContext
  3. On success: sets invalidated = true → commits → releases connection
  4. On error: sets invalidated = true → rolls back → releases connection → re-throws

Safety features:

  • Pre-execution guard — calling tx.execute() after the transaction ends throws RUNTIME.TRANSACTION_CLOSED
  • Iteration guard — a guarded AsyncGenerator wraps every result; consuming rows post-commit throws the same error per-yield
  • Rollback failure wrapping — if rollback itself fails, produces RUNTIME.TRANSACTION_ROLLBACK_FAILED with the original error as .cause

PostgresClient.transaction() + PostgresTransactionContext

export interface PostgresTransactionContext<TContract extends Contract<SqlStorage>>
  extends TransactionContext {
  readonly sql: Db<TContract>;
}

db.transaction(fn) delegates to withTransaction, attaching a fresh Db<TContract> SQL builder as tx.sql. Lazy-initializes the runtime on first call (same as connect() / runtime()).

New public exports from @prisma-next/sql-runtime

  • TransactionContext (type)
  • RuntimeConnection, RuntimeTransaction, RuntimeQueryable (types — previously internal)
  • withTransaction (function)

Tests

Unit tests — withTransaction (12 tests)

Test Covers
commits on successful callback and returns the result Happy path
rolls back on callback error and re-throws Error path
releases connection after commit Cleanup on success
releases connection after rollback Cleanup on failure
propagates commit failure COMMIT throws
forwards the callback return value Complex return type
executes queries against the transaction Routing to transaction scope
throws on execute after commit (invalidation) Post-commit guard
throws on iteration of escaped AsyncIterableResult after commit Iteration guard
sets invalidated flag after commit Flag transition
wraps original error when rollback fails Double-fault handling
sets invalidated flag after rollback Flag on error path
releases connection independently across sequential transactions 3 sequential txns

Unit tests — Postgres (3 tests)

  • transaction() delegates to withTransaction with the lazy runtime
  • transaction() provides sql on the transaction context
  • transaction() lazily creates runtime before connect()

Type-level tests (3 tests)

  • Transaction context does not expose a transaction method (no nesting)
  • db.transaction() infers callback return type correctly
  • tx.sql has the same type as db.sql

E2E tests against real Postgres (5 tests)

  • Commits both writes atomically (two INSERTs visible after commit)
  • Rolls back all writes on error (0 rows after throw)
  • Forwards callback return value after commit
  • Collects returned stream before commit (PromiseLike auto-drain)
  • Rejects escaped AsyncIterableResult consumed after commit

Key design decisions

  • Callback-scoped (RAII-style) — guaranteed cleanup, no dangling transactions
  • Invalidation over buffering — consistent lazy execute() semantics everywhere (ADR 187)
  • Structured error codesRUNTIME.TRANSACTION_CLOSED, RUNTIME.TRANSACTION_ROLLBACK_FAILED
  • Layered composition — core helper in sql-runtime, adapter adds tx.sql surface
  • No nested transactions by type designPostgresTransactionContext omits transaction, enforced at compile time
  • PromiseLike<R> callback return — auto-drains AsyncIterableResult when returned directly

Files changed

  • packages/2-sql/5-runtime/src/sql-runtime.tswithTransaction, TransactionContext
  • packages/2-sql/5-runtime/src/exports/index.ts — new exports
  • packages/2-sql/5-runtime/test/sql-runtime.test.ts — 12 unit tests
  • packages/3-extensions/postgres/src/runtime/postgres.tsdb.transaction(), PostgresTransactionContext
  • packages/3-extensions/postgres/test/postgres.test.ts — 3 unit tests
  • packages/3-extensions/postgres/test/transaction.types.test-d.ts — 3 type tests (new)
  • test/e2e/framework/test/transaction.test.ts — 5 e2e tests (new)
  • projects/orm-client-transaction-api/{plan,spec}.md — mark M1 tasks done

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 1c273b8f-881e-44e0-a9a0-867303fcb75a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch transaction-api-m1

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 15, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

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

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

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@346

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-pipeline-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-pipeline-builder@346

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: f4da1a0

@aqrln aqrln marked this pull request as ready for review April 15, 2026 20:04
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