cockroachdb / cockroachdb/cockroach

sql: a statement error closes all open cursors, not just the one that errored

Open
#174,327 1 comment 0 reactions 1 assignee Claimed by @DrewKimball View on GitHub
A-sql-executor A-sql-pgcompat C-bug O-agent T-sql-queries
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.