Unsettled transaction pipeline promise on parameter validation error
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 8.7k
- Forks
- 374
- Avg merge
- 11d 16h
- Merged PRs (30d)
- 1
Description
The library has a great feature where multiple statements can be returned as an array from a transaction block, causing them to be pipelined. However, if an error is thrown by the library during parameter handling for any statement other than the first in that array, the resulting promise will never settle.
Library version: 3.4.7
Node version: 22.16.0
This example will hang indefinitely:
const postgres = require('postgres')
const db = postgres({
host: 'localhost',
database: 'test',
username: 'test',
password: 'test',
port: 5432,
})
async function main() {
await db.begin(sql => [
sql`select 1`,
sql`select ${undefined}`,
])
}
main().finally(() => db.end())
Removing the first statement (select 1) will cause the code to immediately throw an UNDEFINED_VALUE error, as intended.
Reversing the order of the statements will also cause the code to throw an UNDEFINED_VALUE error, as intended.
Just to be clear: Errors that occur during the execution of the statement by PostgreSQL will not result in the described behaviour; this issue only concerns errors that are thrown by the library when "building" the statement. I'm not sure if there are errors other than UNDEFINED_VALUE that can be thrown during that phase.
I'm guessing it could be caused by starting the transaction and issuing the first statement before completing the parameter handling for all queries in the batch. Patching src/index.js to eagerly call src/connection.js's non-exported build function for every statement before awaiting/executing them does resolve the issue:
// ------------------------------------ 8< ------------------------------------
async function scope(c, fn, name) {
const sql = Sql(handler)
sql.savepoint = savepoint
sql.prepare = x => prepare = x.replace(/[^a-z0-9$-_. ]/gi)
let uncaughtError
, result
name && await sql`savepoint ${sql(name)}`
try {
result = await new Promise((resolve, reject) => {
const x = fn(sql)
+ // Exporting the `build` function from `src/connection.js` and calling
+ // it for each statement will throw an error immediately, and avoid
+ // hanging the promise.
+ if (Array.isArray(x)) {
+ x.forEach(y => connection.build(y))
+ }
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve, reject)
})
if (uncaughtError)
throw uncaughtError
} catch (e) {
// ------------------------------------ >8 ------------------------------------
I'm unsure of what fix is appropriate:
- Something similar to the fix above (while probably also avoiding calling
buildtwice for eachQueryinstance). - Introducing error handling somewhere to abort the transaction.
- Something else entirely?
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the hanging transaction with the issue's array of statements, then read the transaction handling in src/index.js and the build function in src/connection.js. Trace parameter-validation errors for later pipelined statements and add regression coverage; done means the promise rejects with the expected error instead of remaining unsettled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js, postgres
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100