Skip to content

docs: add transaction API related spec, plan and ADR#345

Open
aqrln wants to merge 1 commit intomainfrom
transaction-api-spec
Open

docs: add transaction API related spec, plan and ADR#345
aqrln wants to merge 1 commit intomainfrom
transaction-api-spec

Conversation

@aqrln
Copy link
Copy Markdown
Member

@aqrln aqrln commented Apr 15, 2026

PR Stack: Transaction API

PR Description Base
#345 (this) 📄 Spec, plan, and ADR main
#346 ⚙️ 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

Adds the foundational design documents for the ORM Client Transaction API (db.transaction(callback)):

  • Spec — full feature specification for db.transaction() on PostgresClient
  • Plan — 4-milestone delivery plan with acceptance criteria traceability matrix
  • ADR 187 — architecture decision on how transaction-scoped streams handle post-commit consumption

Spec highlights

The API follows a callback-scoped pattern — no dangling transactions, guaranteed cleanup:

await db.transaction(async (tx) => {
  await tx.orm.account.where({ id: fromId }).update({ balance: fromBalance - amount });
  const plan = tx.sql.from(tables.auditLog).insert({ action: 'transfer', amount }).build();
  await tx.execute(plan);
});

The transaction context exposes three surfaces:

  • tx.orm — full ORM collection API bound to the transaction connection
  • tx.sql — typed SQL builder (Db<TContract>) bound to the same connection
  • tx.execute(plan) — execute a built plan directly

Explicit non-goals: configurable isolation levels, savepoints/nesting, automatic retry, interactive begin/commit/rollback handle, timeout, MongoDB support.

Plan

Milestone Scope Status
M1 — Transaction runtime plumbing withTransaction helper, db.transaction(), tx.sql, unit + e2e tests ✅ Done
M2 — ORM integration tx.orm wiring, withMutationScope fallback, nested-mutation e2e tests ✅ Done
M3 — Type safety & edge cases Type-level tests, PromiseLike drain, post-commit invalidation, lazy connect ☐ Pending
M4 — Close-out Acceptance criteria verification, doc finalization, project cleanup ☐ Pending

ADR 187: Transaction-scoped streams use invalidation, not buffering

Decision: When a transaction ends, the scoped RuntimeQueryable sets an invalidated flag. Any AsyncIterableResult consumed after that point throws a clear error rather than hitting a released connection.

Rejected alternatives: eager buffering (silently changes semantics), type-level prevention (breaks inference), pre-commit tracking (rejects valid patterns), do nothing (confusing errors).

The ADR includes a wire-protocol safety analysis confirming that unconsumed result streams cannot hide database errors or cause an incorrect COMMIT in Postgres.

Files

  • projects/orm-client-transaction-api/spec.md (new)
  • projects/orm-client-transaction-api/plan.md (new)
  • docs/architecture docs/adrs/ADR 187 - Transaction-scoped streams use invalidation not buffering.md (new)

Summary by CodeRabbit

  • Documentation
    • Added guidance on transaction-scoped streaming behavior, including error handling for results accessed outside the transaction scope and best practices for consuming results within transactions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

A new architectural decision record (ADR 187) has been added documenting transaction-scoped streaming behavior. The design uses invalidation flags and runtime error messages to prevent consuming lazy AsyncIterableResult objects after transaction completion, rather than employing buffering or type-level restrictions.

Changes

Cohort / File(s) Summary
Transaction-Scoped Streaming ADR
docs/architecture/adrs/ADR 187 - Transaction-scoped streams use invalidation not buffering.md
New architectural decision document detailing transactional streaming strategy using invalidation-based error handling, with analysis of rejected alternatives and error scenario safeguards.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A stream within a transaction's care,
Once the dance is done, beware!
Invalidate with grace and flair,
Clear errors guide the developer's prayer,
No buffering burden to bear!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title partially relates to the changeset. While it mentions 'transaction API related spec, plan and ADR', the raw summary and PR objectives detail that the changeset includes 3 files: spec.md, plan.md, and ADR 187. The title is somewhat accurate but overly broad in phrasing—it doesn't clearly distinguish which specific ADR is being added or the primary architectural decision being documented.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch transaction-api-spec

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@345

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

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

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

@prisma-next/runtime-executor

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-pipeline-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: bf9f7c0

@aqrln aqrln marked this pull request as ready for review April 15, 2026 20:03
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/architecture` docs/adrs/ADR 187 - Transaction-scoped streams use
invalidation not buffering.md:
- Around line 50-58: Trim the implementation-specific driver internals from "ADR
187 - Transaction-scoped streams use invalidation not buffering": remove
sentences that describe low-level library behavior (e.g., the node-postgres
socket handling, tokio-postgres futures/polling model, and exact cursor
execution mechanics) and replace them with a concise decision-level statement
that the ADR assumes drivers will eagerly surface server errors and that cursor
semantics are a separate concern; move the removed details into a linked
investigation or subsystem doc (e.g., "driver-internals" notes) and add a brief
pointer/link from the ADR to that doc so readers can follow up for
implementation specifics.
- Around line 19-49: Add a single framing sentence immediately before the
"Alternatives considered" list that states the explicit comparison axis (the
invariant/trade-off) being evaluated — e.g., whether each option favors
preserving lazy/streaming semantics inside transactions versus enforcing eager
safety and runtime/type complexity — so readers understand that all alternatives
are being compared on the same knob; place this sentence above the "Eager
buffering inside transactions" subsection and reference the options named there
("Eager buffering inside transactions", "Type-level prevention", "Runtime error
before commit (tracking approach)", "Do nothing (document the pitfall)") to make
clear which approaches are being evaluated against that axis.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ffda6f75-9532-43db-9bd2-d87b0a8762b1

📥 Commits

Reviewing files that changed from the base of the PR and between 69226a5 and bf9f7c0.

⛔ Files ignored due to path filters (2)
  • projects/orm-client-transaction-api/plan.md is excluded by !projects/**
  • projects/orm-client-transaction-api/spec.md is excluded by !projects/**
📒 Files selected for processing (1)
  • docs/architecture docs/adrs/ADR 187 - Transaction-scoped streams use invalidation not buffering.md

Comment on lines +19 to +49
## Alternatives considered

### Eager buffering inside transactions

`tx.execute` would internally drain all rows to an array, then wrap them in a pre-materialized `AsyncIterableResult`. Same return type, no leak possible.

**Rejected because:** it silently changes `execute()` semantics inside transactions (always eager) vs outside (lazy/streaming). This is surprising, increases memory pressure, and creates a behavioral difference that is invisible at the type level. Streaming inside transactions is not inherently wrong — the user may want to process a large result set row-by-row within the transaction without materializing all rows in memory.

### Type-level prevention

Constrain the callback's return type to exclude `AsyncIterableResult`:

```typescript
type NoAsyncIterable<T> = T extends AsyncIterable<unknown> ? never : T;
transaction<R>(fn: (tx: TxCtx) => PromiseLike<NoAsyncIterable<R>>): Promise<R>;
```

**Rejected because:** conditional types on generic return types interact poorly with inference — `R` may resolve to `never` instead of producing a useful error. Cannot catch `AsyncIterableResult` nested inside returned objects. Adds type complexity without runtime safety.

### Runtime error before commit (tracking approach)

Track every `AsyncIterableResult` created by the transaction. Before commit, check if any are unconsumed and throw.

**Rejected because:** it is an error to have an unconsumed stream only if the user tries to read from it after the transaction — an unconsumed stream that is simply discarded is harmless. Throwing before commit would reject valid patterns where the user intentionally ignores a result.

### Do nothing (document the pitfall)

The direct return case (`return tx.execute(plan)`) is already safe because `PromiseLike` causes auto-drain. Only the "wrap in object" case is hazardous.

**Rejected because:** `return { users: tx.execute(plan1), posts: tx.execute(plan2) }` is a natural and common pattern. Relying on documentation for a failure mode that produces confusing errors (connection gone, recycled connection) is insufficient.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the alternatives comparison axis explicit.

This section compares options, but it does not explicitly state the shared axis (invariant/trade-off) up front. Add one short framing sentence before the alternatives list.

Suggested doc tweak
 ## Alternatives considered
 
+Compared on a shared axis: semantic consistency of `execute()`, runtime safety, memory profile, and developer ergonomics.
+
 ### Eager buffering inside transactions

As per coding guidelines: "Research comparisons must explicitly state the shared axis (knob/invariant/trade-off) being compared."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/architecture` docs/adrs/ADR 187 - Transaction-scoped streams use
invalidation not buffering.md around lines 19 - 49, Add a single framing
sentence immediately before the "Alternatives considered" list that states the
explicit comparison axis (the invariant/trade-off) being evaluated — e.g.,
whether each option favors preserving lazy/streaming semantics inside
transactions versus enforcing eager safety and runtime/type complexity — so
readers understand that all alternatives are being compared on the same knob;
place this sentence above the "Eager buffering inside transactions" subsection
and reference the options named there ("Eager buffering inside transactions",
"Type-level prevention", "Runtime error before commit (tracking approach)", "Do
nothing (document the pitfall)") to make clear which approaches are being
evaluated against that axis.

Comment on lines +50 to +58
## Why this is safe: wire protocol analysis

A separate concern was whether unconsumed result streams could hide database errors (e.g., constraint violations) within a transaction, allowing a COMMIT that should have been a ROLLBACK. Investigation confirmed this is not an issue:

- **Not a wire protocol issue.** The Postgres wire protocol is push-oriented — the server sends `ErrorResponse` immediately. The delayed error surfacing observed in the Rust tokio-postgres driver was an architectural issue specific to that library's futures/polling model, not a protocol limitation.
- **node-postgres reads eagerly.** The `pg` library attaches `stream.on('data', ...)` to the socket. The event loop drains all server responses regardless of whether user code has consumed the result.
- **DML executes fully.** INSERT/UPDATE/DELETE (with or without RETURNING) executes atomically — cursors (DECLARE CURSOR) are SELECT/VALUES-only. Mutation errors are always surfaced.
- **Failed transaction state is server-side.** If any statement errors, the server marks the transaction as aborted. A subsequent COMMIT returns the command tag ROLLBACK regardless of whether the client read the original error.
- **Cursors defer execution, not errors.** With cursors, unfetched SELECT rows are genuinely never evaluated by the server — this is not error suppression but incomplete execution. For pure reads, this is harmless. For SELECT FOR UPDATE, locks are acquired per-FETCH, but this is only meaningful if the user reads the results to act on them within the same transaction — partial consumption of a FOR UPDATE query is already an application logic bug independent of the streaming API.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Trim implementation-specific driver internals from ADR scope.

This section includes low-level library behavior details (e.g., exact pg socket handling) that read like implementation notes. Keep ADR text at decision/constraint level and move deep driver mechanics to subsystem docs or linked investigation notes.

Based on learnings: "ADRs in prisma/prisma-next should document architectural design decisions and the key constraints/assumptions only. Do not include implementation algorithms...".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/architecture` docs/adrs/ADR 187 - Transaction-scoped streams use
invalidation not buffering.md around lines 50 - 58, Trim the
implementation-specific driver internals from "ADR 187 - Transaction-scoped
streams use invalidation not buffering": remove sentences that describe
low-level library behavior (e.g., the node-postgres socket handling,
tokio-postgres futures/polling model, and exact cursor execution mechanics) and
replace them with a concise decision-level statement that the ADR assumes
drivers will eagerly surface server errors and that cursor semantics are a
separate concern; move the removed details into a linked investigation or
subsystem doc (e.g., "driver-internals" notes) and add a brief pointer/link from
the ADR to that doc so readers can follow up for implementation specifics.

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