porsager / porsager/postgres

Unsettled transaction pipeline promise on parameter validation error

Open
#1,082 1 comment 0 reactions 0 assignees View on GitHub

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 build twice for each Query instance).
  • Introducing error handling somewhere to abort the transaction.
  • Something else entirely?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.