From f7cc952e46512e90b042579bfe84f3d24181e790 Mon Sep 17 00:00:00 2001 From: reinierlakhan Date: Wed, 2 Sep 2026 16:51:39 -0400 Subject: [PATCH] Run onexecute regardless of max_pipeline so begin() works at 0 execute() returned a single && chain that did two unrelated jobs: deciding whether the pool may pipeline another query onto this connection, and running the query's onexecute hook. begin() relies on that hook to capture the connection and move it to the reserved queue. With max_pipeline: 0 the `sent.length < max_pipeline` term is always false, so the chain short-circuited before onexecute ran. The connection was never reserved and the BEGIN's CommandComplete guard rejected every transaction with UNSAFE_TRANSACTION. Run the hook whenever the query was actually written, and keep the return value's meaning ("may the pool pipeline more onto this connection") unchanged. For max_pipeline > 0 behaviour is identical: onexecute returns a truthy value, so the old chain was truthy exactly when the new one is. For max_pipeline: 0 execute() now returns false after reserving, the pool moves the connection to full, and on the BEGIN's ReadyForQuery connection.reserved() drains the transaction's own queue or moves it back to reserved, so nothing is pipelined. --- src/connection.js | 5 +++-- tests/index.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/connection.js b/src/connection.js index 10ab1bb3..790c8772 100644 --- a/src/connection.js +++ b/src/connection.js @@ -170,11 +170,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose : (query = q, query.active = true) build(q) - return write(toBuffer(q)) + const written = write(toBuffer(q)) + written && q.options.onexecute && q.options.onexecute(connection) + return written && !q.describeFirst && !q.cursorFn && sent.length < max_pipeline - && (!q.options.onexecute || q.options.onexecute(connection)) } catch (error) { sent.length === 0 && write(Sync) errored(error) diff --git a/tests/index.js b/tests/index.js index 845c6ce0..7b71cb7c 100644 --- a/tests/index.js +++ b/tests/index.js @@ -332,6 +332,27 @@ t('Helpers in Transaction', async() => { ))[0].x] }) +t('Transaction works with max_pipeline 0', async() => { + const sql = postgres({ ...options, max: 4, max_pipeline: 0 }) + return [1, (await sql.begin(sql => sql`select 1 as x`))[0].x, await sql.end()] +}) + +t('Transaction with sequential queries works with max_pipeline 0', async() => { + const sql = postgres({ ...options, max: 4, max_pipeline: 0 }) + return ['testing', await sql.begin(async sql => { + await sql`select set_config('postgres_js.test', 'testing', true)` + return (await sql`select current_setting('postgres_js.test') as x`)[0].x + }), await sql.end()] +}) + +t('Transaction with concurrent queries works with max_pipeline 0', async() => { + const sql = postgres({ ...options, max: 4, max_pipeline: 0 }) + return ['testing', (await sql.begin(sql => [ + sql`select set_config('postgres_js.test', 'testing', true)`, + sql`select current_setting('postgres_js.test') as x` + ]))[1][0].x, await sql.end()] +}) + t('Undefined values throws', async() => { let error