sql-pg: send several statements in one extended-query Sync (transaction-local context without extra round trips)
メンテナーはふだん 1 日以内に返信
@effect-bot がすでに取り組んでいます。
2026年10月29日 から。
評価
この issue はまだ評価されていません。
説明
What is the problem this feature would solve?
We run Postgres row-level security behind PgBouncer in transaction mode. Session state cannot be trusted there, so every operation sets its context transaction-locally with set_config(..., true) and then runs its query.
With @effect/sql-pg 4.0.6, every statement on a pinned connection or in a transaction is its own extended-query cycle: one Sync, then a wait for ReadyForQuery. A single scoped query therefore costs four round trips:
import { SqlClient } from "effect/sql/SqlClient"
import { Effect } from "effect"
const scoped = (org: string) =>
Effect.gen(function* () {
const sql = yield* SqlClient
return yield* sql.withTransaction(
Effect.gen(function* () {
yield* sql`SELECT set_config('app.organization_id', ${org}, true)`
return yield* sql`SELECT * FROM item`
})
)
})
On the wire: BEGIN + Sync, set_config + Sync, SELECT + Sync, COMMIT + Sync. The query itself is one of four round trips.
What we measured (PostgreSQL 18, PgBouncer 1.25.1 transaction mode, 0.1 ms of added latency per direction per link, pgbench extended protocol, 10 server connections):
| latency, 1 client | TPS, 32 clients | |
|---|---|---|
| plain query, no context | 0.613 ms | — |
| BEGIN / set_config / query / COMMIT, one Sync each | 2.303 ms (+276%) | 3,568 |
| set_config + query, one Sync (implicit transaction) | 0.686 ms | 16,707 (4.7×) |
Putting the context statement and the query in one Sync recovers about 96% of the overhead. With one Sync, PostgreSQL runs the whole cycle as one implicit transaction: set_config(..., true) dies with it, and an error in the context statement skips the query. PgBouncer also gets the server connection back after one round trip instead of four, and that is where the 4.7× throughput comes from. We checked 400 alternations between organizations on shared PgBouncer backends and saw no context leak.
Explicit transactions benefit too. BEGIN plus the context statement in one Sync saves one round trip per transaction. The core SqlClient sends BEGIN on its own through executeUnprepared, so we cannot merge it from outside.
The extended-query protocol allows any number of Parse/Bind/Execute groups before one Sync. PgConnection has no API that uses this today, as far as we can tell: query, queryValues, executeUnprepared and the multiplexed pipeline each close every statement with its own Sync.
What is the feature you are proposing to solve the problem?
An API on PgConnection that sends one or more "prelude" statements in the same cycle as a query:
interface PreludeStatement {
readonly sql: string
readonly params?: ReadonlyArray<unknown>
}
readonly queryWithPrelude: (
prelude: ReadonlyArray<PreludeStatement>,
sql: string,
params?: ReadonlyArray<unknown>,
prepare?: boolean
) => Effect.Effect<Result, SqlError>
- Each prelude statement is unnamed
Parse/Bind/Execute, with noDescribeand noSync. The query's own frame follows, and oneSynccloses the cycle: one write, one round trip. - The query's rows come back. Prelude replies are consumed and their rows dropped.
- A prelude
ErrorResponsefails the effect with that error. The backend skips the rest of the cycle, so the query never runs. - A multiplexed connection pins for the cycle, so nothing else gets between the prelude and the query.
- A retry after a stale prepared statement sends the prelude again, because the failed cycle ran as one unit.
Higher up, SqlClient could expose a hook so that withTransaction sends its BEGIN together with caller-supplied statements in one Sync. This is related to #9573 / #9576, which make the BEGIN statement configurable.
We run this today as a pnpm patch against @effect/sql-pg 4.0.6. The patch adds about 160 lines to dist/PgConnection.js: an encodePrelude helper, a small PreludeMachine that consumes prelude replies and hands everything else to the existing QueryMachine, and an optional prelude argument on runQuery. We can contribute it if the shape fits. On top of it, a standalone scoped query is queryWithPrelude([context], query) with no BEGIN/COMMIT. An explicit transaction sends BEGIN ISOLATION LEVEL ... as the prelude and the context statement as the query. If that cycle fails, our wrapper issues a ROLLBACK itself, because the core only rolls back after a successful begin.
What alternatives have you considered?
- Session-level
SET/ connection-start GUCs. Unsafe behind PgBouncer transaction mode: the next client on the same backend inherits the settings. - A multi-statement simple query (
BEGIN; SELECT set_config('...', 'literal', true)). One round trip, but no bind parameters: values must be escaped by hand, and the query that follows still needs its own cycle. - The multiplexed pipeline (
multiplex: true). It pipelines independent statements, but every statement still gets its ownSync, so each one is its own implicit transaction. Transaction-local settings do not survive to the next statement. Transactions pin the connection and run one cycle at a time anyway. - Folding
set_configinto each query (CTE or function call). It rewrites every query, the order of evaluation against the RLS policy is not guaranteed, and it cannot carrystatement_timeoutorlock_timeoutfor that same statement. - Accepting the cost. At four round trips per scoped query, it is the largest per-query cost in our service: about 1.7 ms of 2.3 ms in the measurement above.
- 主要言語
- TypeScript
- スター
- 16.7k
- フォーク
- 808
- 平均マージ
- 11時間 11分
- マージ済み PR(30日)
- 840
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
Effect-TS/effect のほかの issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
@effect/ai-anthropic: text_editor_20250728 insert decodes `new_str`, but the API sends `insert_text`オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 1 日以内に返信
-
[email protected]: crypto/KeyFormat declarations require the global JsonWebKey, breaking Node-only type-checks対応中かも @effect-bot が今日担当しました。 オープン
Effect-TS/effect#10115 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 4/5 1〜2日 初心者へのやさしさ 55/100
メンテナーはふだん 1 日以内に返信
-
CircuitBreaker: record an outcome without guarding the call対応中かも @effect-bot が今日担当しました。 オープン
Effect-TS/effect#10110 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
Effect-TS/effect の issue をすべて見る
似ている issue
-
Parallel reticle_verify opens per-flow leases without the source lease's ignoreHTTPSErrors対応中かも @as992949791 が今日担当しました。 オープンgood first issue help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
reticlehq/reticle#2033 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
area:quality p3-medium ready-for-agent type:docs
難易度 2/5 1〜3時間 初心者へのやさしさ 83/100
fderuiter/deruiter.dev#3589 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
manfredsteyer/angular-oauth2-oidc#1514 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
marmelab/react-admin#11431 ·
メンテナーはふだん 1 日以内に返信