diff --git a/async_postgres/pg_client/cursor.nim b/async_postgres/pg_client/cursor.nim index 83a874b..243c98f 100644 --- a/async_postgres/pg_client/cursor.nim +++ b/async_postgres/pg_client/cursor.nim @@ -131,6 +131,8 @@ proc openCursorImpl( proc fetchNextImpl(cursor: Cursor): Future[seq[Row]] {.async.} = let conn = cursor.conn + if conn.state == csClosed: + raise newException(PgConnectionError, "Connection is closed") let rd = newRowData(int16(cursor.fields.len), cursor.colFormats, cursor.colTypeOids) rd.fields = cursor.fields var rowCount: int32 = 0 @@ -201,6 +203,9 @@ proc fetchNext*(cursor: Cursor): Future[seq[Row]] {.async.} = ## Fetch the next chunk of rows from the cursor. ## Returns an empty seq when the cursor is exhausted. ## On timeout, the connection is marked csClosed (protocol out of sync). + let conn = cursor.conn + if conn.state == csClosed: + raise newException(PgConnectionError, "Connection is closed") if cursor.bufferedCount > 0: result = newSeq[Row](cursor.bufferedCount) for i in 0 ..< cursor.bufferedCount: diff --git a/tests/test_abandonment_e2e.nim b/tests/test_abandonment_e2e.nim index 355842c..15c87b3 100644 --- a/tests/test_abandonment_e2e.nim +++ b/tests/test_abandonment_e2e.nim @@ -65,6 +65,21 @@ suite "E2E: Cursor lifecycle invariants": waitFor t() + test "fetchNext after conn.close() raises PgConnectionError, not SIGSEGV": + proc t() {.async.} = + let conn = await connect(plainConfig()) + let cursor = + await conn.openCursor("SELECT i FROM generate_series(1, 100) i", chunkSize = 10) + await conn.close() + var raised = false + try: + discard await cursor.fetchNext() + except PgConnectionError: + raised = true + doAssert raised, "fetchNext after close must raise PgConnectionError" + + waitFor t() + test "openCursor with invalid SQL raises and leaves connection ready": # Recovery path: ErrorResponse during openCursor drains Sync back to # ReadyForQuery so the connection is reusable immediately.