PgClient: withTransaction dies instead of failing with SqlError when the server ends the session

クローズ
#10,290 コメント 0 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@effect-bot がすでに取り組んでいます。

2026年11月2日 から。

  • #10291 @0rierel による — マージ済み

評価

この issue はまだ評価されていません。

説明

What version of Effect is running?

[email protected], @effect/[email protected], PostgreSQL 14, Node 26.3.1. Reproduced on main (506eef1).

What steps can reproduce the bug?

When the server ends the session inside withTransaction (pg_terminate_backend, an administrator's kill, idle_in_transaction_session_timeout, a dropped connection), the statement fails with a typed SqlError. The server has already rolled the transaction back with the session. withTransaction then issues its own ROLLBACK on the closed connection, which can only fail, and that error is added to the exit as a defect.

import { PgClient } from "@effect/sql-pg"
import { Effect, Redacted } from "effect"
import * as Reactivity from "effect/reactivity/Reactivity"

const program = Effect.gen(function*() {
  const sql = yield* PgClient.make({ url: Redacted.make("postgres://postgres:postgres@localhost:5432/postgres") })

  const exit = yield* Effect.exit(sql.withTransaction(sql`SELECT pg_terminate_backend(pg_backend_pid())`))
  for (const r of exit._tag === "Failure" ? exit.cause.reasons : []) {
    const e = r._tag === "Fail" ? r.error : r._tag === "Die" ? r.defect : undefined
    console.log(`${r._tag}: ${e?.reason?._tag} ${e?.reason?.cause?.message}`)
  }

  const recovered = yield* sql.withTransaction(sql`SELECT pg_terminate_backend(pg_backend_pid())`).pipe(
    Effect.as("not reached"),
    Effect.catchTag("SqlError", () => Effect.succeed("recovered")),
    Effect.catchDefect(() => Effect.succeed("died"))
  )
  console.log(`catchTag("SqlError"): ${recovered}`)
})

Effect.runPromise(Effect.scoped(program).pipe(Effect.provide(Reactivity.layer)))
Die: UnknownError terminating connection due to administrator command
Die: ConnectionError Connection closed
catchTag("SqlError"): died
What is the expected behavior?

The exit fails with the statement's SqlError only, and Effect.catchTag("SqlError", ...) recovers. With PgClient's pool, the next transaction runs on a fresh session.

What do you see instead?

The ROLLBACK failure is a defect. Since #9433 it takes precedence over the body's typed failure, so the exit is Die(UnknownError) + Die(ConnectionError), catchTag("SqlError", ...) no longer runs, and the transaction dies even when the caller handles SqlError. A nested withTransaction fails the same way at ROLLBACK TO SAVEPOINT.

Additional information

This is the PostgreSQL side of #9586. On failure, makeWithTransaction runs Effect.orDie over the rollback and savepoint cleanup (SqlClient.ts#L699-L709). #9599 added an isTransaction predicate to SqlClient.make that skips that cleanup once no transaction is open (#L317-L334), but only the SQLite clients pass it. PgClient does not (PgClient.ts#L314-L334).

The connection already tracks what the predicate needs: deadWith and transactionStatus, updated from each ReadyForQuery (PgConnection.ts#L392-L396, #L455). The public PgConnection interface exposes neither (#L213).

Fix idea: expose PgConnection.inTransaction (false once the session has ended) and pass isTransaction from PgClient. A ROLLBACK on a live session, aborted or not, still runs, and its failures stay defects.

主要言語
TypeScript
スター
16.7k
フォーク
808
平均マージ
12時間 16分
マージ済み PR(30日)
809

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

Effect-TS/effect のほかの issue

Effect-TS/effect の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。