Skip to content

Reset row index after a query errors mid-result - #1254

Open
pislm wants to merge 1 commit into
porsager:masterfrom
pislm:reset-row-index-after-error
Open

pislm wants to merge 1 commit into
porsager:masterfrom
pislm:reset-row-index-after-error

Conversation

@pislm

@pislm pislm commented Oct 12, 2026

Copy link
Copy Markdown

Fixes #1253

DataRow writes result[rows++] = row, and rows is reset only in CommandComplete. A query that errors after some rows have arrived never reaches CommandComplete. Examples: a cancelled query, or a runtime error like division by zero on a later row. ReadyForQuery then gives the connection a fresh Result but keeps the old rows, so the next query on that connection starts writing at that offset and its result begins with empty slots.

const sql = postgres({ max: 1 })
await sql`select 1 / (g - 3) from generate_series(1, 5) g`.catch(x => x) // 22012 after 2 rows
await sql`select 1 as x` // before: [ <2 empty items>, { x: 1 } ], after: [ { x: 1 } ]

This resets rows in ReadyForQuery, next to result = new Result(), and adds a test that uses the division-by-zero case so it doesn't depend on timing.

🤖 Generated with Claude Code

A query that errors after some DataRows (cancelled, or a runtime error
such as division by zero) never reaches CommandComplete, so the row index
was left as is and the next query on the connection started writing its
rows at that offset, leaving empty slots at the start of its result.

Fixes porsager#1253

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GrahamCampbell

Copy link
Copy Markdown

We carry this line too. It is the line #1190 adds for #1181, and #1119 reports the same fault, so either pull request closes #1253, #1181 and #1119.

Two more paths it covers, besides a query that fails on a live connection:

Scripts and output

Against PostgreSQL 17. The first prints CONNECTION_CLOSED, 57P01 and length 21 on master, and length 1 as its last line with this line added.

import postgres from 'postgres'

// Server and credentials from PGHOST, PGPORT, PGUSER, PGPASSWORD and PGDATABASE.
const sql = postgres({ max: 1 })
const admin = postgres({ max: 1 })
const [{ pid }] = await sql`select pg_backend_pid() as pid`
const lost = sql`
  select g, repeat('x', 20000), pg_sleep(case when g > 20 then 0.2 else 0 end) from generate_series(1, 100) g
`.catch(e => e.code)
while (!(await admin`select 1 from pg_stat_activity where pid = ${ pid } and wait_event = 'PgSleep'`).length)
  await new Promise(r => setTimeout(r, 20))
await admin`select pg_terminate_backend(${ pid })`
console.log('query under the close:', await lost)
console.log('first query after it :', await sql`select 1 as x`.then(x => 'length ' + x.length, e => e.code))
console.log('second query after it:', await sql`select 1 as x`.then(x => 'length ' + x.length, e => e.code))
await sql.end()
await admin.end()

The second prints 57014 and the seven-place array with #1256 alone, and 57014 and the two rows with both. On master the cancelled query stays pending and the query behind it is rejected with its 57014 (#1252).

import postgres from 'postgres'

// Server and credentials from PGHOST, PGPORT, PGUSER, PGPASSWORD and PGDATABASE.
const sql = postgres({ max: 1 })
await sql`select 1`
sql`select pg_sleep(0.5)`.execute()
const query = sql`
  select g, repeat('x', 20000), pg_sleep(case when g > 5 then 0.3 else 0 end) from generate_series(1, 20) g
`
const cancelled = query.then(x => 'length ' + x.length, e => e.code)
const next = sql`select 'a' as x union all select 'b'`.then(x => JSON.stringify(x), e => e.code)
setTimeout(() => query.cancel(), 100)
const pending = new Promise(r => setTimeout(r, 5000, 'still pending after 5 s'))
console.log('cancelled while piped:', await Promise.race([cancelled, pending]))
console.log('query behind it      :', await Promise.race([next, pending]))
process.exit(0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cancelled query leaves stale row index; next query on the connection returns leading empty slots

2 participants