cockroachdb / cockroachdb/cockroach
sql: a statement error closes all open cursors, not just the one that errored
- Dominant language
- Go
- Stars
- 32.5k
- Forks
- 4.1k
- PR merge metrics
- PR metrics pending
Description
**Describe the problem**
When any statement in an explicit transaction fails, the connExecutor closes *every* open transaction-scoped cursor, not just the cursor (if any) whose execution caused the error. Postgres does not close cursors on a statement error at all: after the transaction is recovered with `ROLLBACK TO SAVEPOINT`, cursors unaffected by the error remain fully usable. Only a cursor whose own execution raised the error is invalidated — and even then it still exists, in a can't-execute state (`ERROR: portal "cbad" cannot be run`), rather than being removed.
This makes cursors unusable with the standard savepoint-based error-recovery pattern: any unrelated statement error silently destroys all cursor state in the transaction.
**To Reproduce**
```sql
CREATE TABLE t (a INT PRIMARY KEY);
INSERT INTO t SELECT generate_series(1, 10);
BEGIN;
INSERT INTO t VALUES (100); -- make the savepoint non-initial
DECLARE c CURSOR FOR SELECT a FROM t ORDER BY a;
SAVEPOINT s;
SELECT 1/0; -- any statement error
ROLLBACK TO SAVEPOINT s;
FETCH 2 FROM c;
```
CockroachDB:
```
ERROR: cursor "c" does not exist
SQLSTATE: 34000
```
PostgreSQL 16.13 returns rows 1 and 2.
For a cursor whose own execution raises the error, PG keeps the cursor but marks it unusable:
```sql
BEGIN;
INSERT INTO t VALUES (101);
DECLARE cbad CURSOR FOR SELECT 1/(a-1) FROM t ORDER BY a;
SAVEPOINT s;
FETCH 2 FROM cbad; -- ERROR: division by zero
ROLLBACK TO SAVEPOINT s;
FETCH 1 FROM cbad; -- PG: ERROR: portal "cbad" cannot be run
-- CRDB: ERROR: cursor "cbad" does not exist
```
**Expected behavior**
Matching Postgres: a statement error leaves open cursors intact. After `ROLLBACK TO SAVEPOINT`, cursors that were not involved in the error can continue to be fetched from; a cursor whose own execution raised the error is placed in a can't-execute state instead of being removed.
**Environment**
Reproduced on a recent master build; present since v23.2.
**Additional context**
The blanket close comes from `shouldClosePausablePortalsAndCursors` in `connExecutor.execCmd` (`pkg/sql/conn_executor.go`): every error-payload event closes all pausable portals and all non-committed cursors. It was added in dcf045ea58386899e47c34d8da24e3e15f0b9612 (v23.2) to avoid leftover buffered bytes in `txnState.finishSQLTxn` when pausable portals are open; cursors were swept into the same workaround. A fix likely needs to scope that cleanup to pausable portals (its original target), leave unrelated cursors open, and add the can't-execute state for a cursor whose own execution failed.
Related: #173449, #173505
Jira issue: CRDB-67630
Contributor guide
Assessment
This issue has not been assessed yet.