Skip to content

fix(sql): propagate a failed transaction begin as a typed error - #7236

Merged
tim-smart merged 3 commits into
Effect-TS:mainfrom
roninjin10:fix/sql-with-transaction-begin-failure
Aug 13, 2026
Merged

tim-smart merged 3 commits into
Effect-TS:mainfrom
roninjin10:fix/sql-with-transaction-begin-failure

Conversation

@roninjin10

@roninjin10 roninjin10 commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

fixes #7235

What was broken

makeWithTransaction in packages/effect/src/unstable/sql/SqlClient.ts wrapped the begin/savepoint step together with the transaction body in Effect.exit. A failed BEGIN therefore landed in the failure branch, which issues a ROLLBACK. No transaction was active at that point, so the ROLLBACK failed too, and its Effect.orDie wrapper discarded the original typed SqlError and replaced it with a defect:

DEFECT (untyped, not retryable):
  effect/sql/SqlError: Failed to execute statement
    effect/sql/SqlError/UnknownError: Failed to execute statement
      Error: cannot rollback - no transaction is active

Callers can no longer classify the failure as busy/locked, so busy-retry logic never sees it.

Why it surfaced now

The makeWithTransaction code is unchanged from the betas, but the branch was unreachable: the older sqlite client set neither beginTransaction nor busy_timeout, so a plain deferred BEGIN took no lock and could not fail. The current sqlite client sets beginTransaction: "BEGIN IMMEDIATE" plus PRAGMA busy_timeout for writable connections, so BEGIN itself can fail with SQLITE_BUSY and reach the latent branch.

The fix

begin/savepoint now runs outside the Effect.exit region. Commit and rollback handling applies only after it succeeds. A failed begin/savepoint propagates as its original typed SqlError, and the acquired connection scope is still closed via Effect.onError. Everything else is unchanged: uninterruptibleMask/restore behaviour, span events, Effect.orDie on the commit/rollback that follow a successful begin, and scope closure on every other path.

Tests

New packages/effect/test/unstable/sql/SqlClient.test.ts drives makeWithTransaction with stub transaction commands (no driver needed). The stubs mirror the driver contract: rollback/rollbackSavepoint fail when nothing is active.

  • a failed begin propagates typed, the wrapped effect never runs, and rollback is not called
  • a failed wrapped effect after a successful begin still rolls back and propagates its typed error
  • a failed savepoint in a nested transaction propagates typed without rollbackSavepoint
  • the connection scope is closed when begin fails
  • the success path still commits and returns the value

All five fail-first: before the fix the begin and savepoint cases fail with the cannot rollback - no transaction is active defect.

Verified end-to-end against @effect/sql-sqlite-node with the reproduction from the issue (two connections, busyTimeout: 0). Before: hasDies: true, UnknownError: cannot rollback - no transaction is active. After: a typed SqlError whose reason is LockTimeoutError with isRetryable: true.

Added in follow-up: packages/sql/sqlite-node/test/Client.test.ts now carries the driver-level counterpart, so the reported scenario is guarded in CI rather than only checked by hand. A contending client with PRAGMA busy_timeout = 1 runs withTransaction while another client holds the write lock; the test asserts the cause carries no defect and that the typed error is a SqlError reporting database is locked. It fails on main with expected a typed failure but the cause contains a defect and passes with this change.

`makeWithTransaction` wrapped the `begin` step together with the transaction
body in `Effect.exit`, so a failed `BEGIN` took the rollback branch. No
transaction was active, the `ROLLBACK` failed, and its `Effect.orDie` wrapper
replaced the original typed `SqlError` with a defect
(`cannot rollback - no transaction is active`), so callers could no longer
classify the failure as retryable.

Commit and rollback now run only after `begin` or `savepoint` succeeds. A
failed `begin` or `savepoint` fails with its original `SqlError`, leaves the
wrapped effect unexecuted, and still closes the acquired connection scope.

fixes Effect-TS#7235

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 543dfc9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/ai-anthropic Patch
@effect/ai-openai Patch
@effect/ai-openai-compat Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node Patch
@effect/platform-node-shared Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/vitest Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 6.91 KB 6.91 KB 0.00 KB (0.00%)
batching.ts 9.72 KB 9.72 KB 0.00 KB (0.00%)
brand.ts 6.50 KB 6.50 KB 0.00 KB (0.00%)
cache.ts 10.63 KB 10.63 KB 0.00 KB (0.00%)
config.ts 21.05 KB 21.05 KB 0.00 KB (0.00%)
differ.ts 19.99 KB 19.99 KB 0.00 KB (0.00%)
http-client.ts 21.56 KB 21.56 KB 0.00 KB (0.00%)
logger.ts 10.88 KB 10.88 KB 0.00 KB (0.00%)
metric.ts 8.86 KB 8.86 KB 0.00 KB (0.00%)
optic.ts 6.67 KB 6.67 KB 0.00 KB (0.00%)
pubsub.ts 14.90 KB 14.90 KB 0.00 KB (0.00%)
queue.ts 11.57 KB 11.57 KB 0.00 KB (0.00%)
schedule.ts 10.74 KB 10.74 KB 0.00 KB (0.00%)
schema-class.ts 19.61 KB 19.61 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 29.57 KB 29.57 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 25.81 KB 25.81 KB 0.00 KB (0.00%)
schema-string-transformation.ts 13.49 KB 13.49 KB 0.00 KB (0.00%)
schema-string.ts 10.99 KB 10.99 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.30 KB 15.30 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 21.73 KB 21.73 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 24.20 KB 24.20 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 18.96 KB 18.96 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 18.78 KB 18.78 KB 0.00 KB (0.00%)
schema-toFormatter.ts 18.65 KB 18.65 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 22.81 KB 22.81 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 19.30 KB 19.30 KB 0.00 KB (0.00%)
schema.ts 18.87 KB 18.87 KB 0.00 KB (0.00%)
stm.ts 12.66 KB 12.66 KB 0.00 KB (0.00%)
stream.ts 9.66 KB 9.66 KB 0.00 KB (0.00%)

The unit tests for `makeWithTransaction` drive the transaction control flow
with stubs. Add the driver-level counterpart from issue Effect-TS#7235: a second client
whose `BEGIN IMMEDIATE` cannot take the write lock must fail `withTransaction`
with a typed `SqlError`, not a rollback defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tim-smart
tim-smart merged commit ccae60e into Effect-TS:main Aug 13, 2026
13 of 15 checks passed
roninjin10 added a commit to smithersai/flows-proto that referenced this pull request Aug 14, 2026
…7235

Two DurableWriter contract cases (NodeDatabase two-connection harness
only) and one journal durable-emission case fail on
"cannot rollback - no transaction is active": Effect's
SqlClient.makeWithTransaction issues ROLLBACK even when BEGIN itself
failed under real write-lock contention. Root cause tracked upstream at
Effect-TS/effect#7235, fixed unreleased by Effect-TS/effect#7236. Mark
with it.fails so each flips loudly red the moment flows depends on an
effect release containing the fix.

This branch had an error being deployed

1 failed deployment
fork — 543dfc97 Deployed Aug 13, 2026 by tim-smart via approval-gate #11862
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.

SqlClient makeWithTransaction turns a failed BEGIN into an unrecoverable defect ("cannot rollback - no transaction is active")

2 participants