Skip to content

feat(postgres): wire ORM to transactions#347

Open
aqrln wants to merge 1 commit intotransaction-api-m1from
transaction-api-m2
Open

feat(postgres): wire ORM to transactions#347
aqrln wants to merge 1 commit intotransaction-api-m1from
transaction-api-m2

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 ⚙️ Core transaction runtime + db.transaction() + tx.sql #345
#347 (this) 🔗 Wire tx.orm into transactions #346

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


Summary

Implements Milestone 2 of the transaction API: wiring tx.orm so that ORM collections used inside a db.transaction() callback execute against the transaction connection. Also adds relations to the e2e fixture contract to enable nested-mutation integration tests.

Implementation

The production change is surgical — ~12 lines in a single file.

PostgresTransactionContext gains orm

export interface PostgresTransactionContext<TContract extends Contract<SqlStorage>>
  extends TransactionContext {
  readonly sql: Db<TContract>;
  readonly orm: OrmClient<TContract>;  // ← new
}

Transaction-scoped ORM client construction

Inside transaction(), a new ormBuilder() creates an ORM client bound to the transaction's execute:

const txOrm: OrmClient<TContract> = ormBuilder({
  runtime: {
    execute(plan) {
      return txCtx.execute(plan);
    },
  },
  context,
});

Key design decision: The runtime object passed to ormBuilder exposes only execute() — it deliberately omits connection() and transaction(). This ensures withMutationScope in the mutation executor naturally falls through to direct scope reuse instead of attempting a nested transaction. Zero changes to the mutation executor were needed.

Tests

Unit tests (4 new)

Test Location Covers
transaction() provides orm on the tx context postgres.test.ts tx.orm is the transaction-scoped proxy
tx.orm has the same type as db.orm transaction.types.test-d.ts Compile-time type equivalence
executeNestedCreateMutation() reuses scope when runtime lacks transaction mutation-executor.test.ts Fast-path: direct execute on the transaction runtime
withMutationScope reuses runtime when no transaction or connection exists mutation-executor.test.ts Scope reuse without nested transaction

E2E integration tests (5 new — real PGlite database)

Test Covers
ORM create with nested relation mutation commits atomically tx.orm.Post.create() with nested author: (r) => r.create(...) — both rows exist
ORM create with nested relation mutation rolls back on throw Same nested create + throw — neither row exists
ORM write then read within the same transaction Read-your-own-writes: create() then where().first() sees the row
Multiple ORM operations in a transaction are atomic Two posts + user all commit together
Multiple ORM operations roll back together on throw Same operations + throw — nothing persists

E2E fixture contract changes

The fixture contract gained relations across all three models to enable nested-mutation tests:

Model Relation Type Join
User posts 1:NPost User.idPost.userId
Post author N:1User Post.userIdUser.id
Post comments 1:NComment Post.idComment.postId
Comment post N:1Post Comment.postIdPost.id

The contract definition was refactored to compose models with rel.belongsTo() / rel.hasMany() before passing to defineContract, using lazy refs (() => Comment) for circular relations.

Design patterns

  • Capability elision for scope reuse — the transaction-scoped runtime is a minimal { execute } object; omitting transaction() / connection() makes withMutationScope fall through naturally
  • Interface segregationRuntimeQueryable passed to ormBuilder is a narrowed shape, preventing accidental nested-transaction creation
  • Warm-up pattern for PGlite — e2e helper calls db.orm.User.first() before entering transactions to force contract verification (which needs its own connection) outside the transaction, avoiding deadlock on single-connection PGlite

Files changed

  • packages/3-extensions/postgres/src/runtime/postgres.ts — add tx.orm construction
  • packages/3-extensions/postgres/test/postgres.test.ts — 1 unit test
  • packages/3-extensions/postgres/test/transaction.types.test-d.ts — 1 type test
  • packages/3-extensions/sql-orm-client/test/mutation-executor.test.ts — 2 unit tests
  • test/e2e/framework/test/transaction-orm.test.ts — 5 e2e tests (new)
  • test/e2e/framework/test/fixtures/contract.ts — add relations to fixture
  • test/e2e/framework/test/fixtures/generated/contract.{d.ts,json} — regenerated
  • test/e2e/framework/package.json + pnpm-lock.yaml — add @prisma-next/postgres dev dep
  • projects/orm-client-transaction-api/{plan,spec}.md — mark M2 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: bcb205bf-b901-445c-8dbc-71540ca0a82c

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-m2

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@347

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

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

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

@prisma-next/runtime-executor

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-pipeline-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: c15f5d9

@aqrln aqrln force-pushed the transaction-api-m2 branch from 13fd20e to c15f5d9 Compare April 15, 2026 20:07
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