Skip to content

Commit c0aec6c

Browse files
committed
sql: ensure correct cursor closing behavior
This commit adds cleanup logic so that the container for a persisted cursor is closed if executing the cursor's query resulted in an error. The closing logic is also modified to correctly roll back all cursors in the event of an error during closing. Fixes #142114 Release note: None
1 parent 16f75dd commit c0aec6c

File tree

2 files changed

+107
-39
lines changed

2 files changed

+107
-39
lines changed

pkg/sql/logictest/testdata/logic_test/cursor

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,3 +872,51 @@ statement ok
872872
RESET autocommit_before_ddl
873873

874874
subtest end
875+
876+
# Regression test for #142114 - close the holdable cursor container if there is
877+
# an error while executing the query.
878+
subtest regression_142114
879+
880+
statement error pgcode 22012 pq: sql-cursor: division by zero
881+
DECLARE foo CURSOR WITH HOLD FOR SELECT 1 // 0;
882+
883+
statement ok
884+
DECLARE curs CURSOR WITH HOLD FOR SELECT 100;
885+
886+
statement ok
887+
BEGIN;
888+
889+
statement ok
890+
DECLARE foo CURSOR FOR SELECT 1;
891+
892+
statement ok
893+
DECLARE bar CURSOR WITH HOLD FOR SELECT 2;
894+
895+
statement ok
896+
DECLARE baz CURSOR WITH HOLD FOR SELECT 1 // 0;
897+
898+
statement ok
899+
DECLARE bar2 CURSOR WITH HOLD FOR SELECT 3;
900+
901+
statement ok
902+
INSERT INTO a VALUES (-1, -2);
903+
904+
statement error pgcode 22012 pq: sql-cursor: division by zero
905+
COMMIT;
906+
907+
# All cursors created in the aborted transaction should be closed.
908+
query T rowsort
909+
SELECT name FROM pg_cursors;
910+
----
911+
curs
912+
913+
# The insert should not have been committed.
914+
query III
915+
SELECT * FROM a ORDER BY a LIMIT 1;
916+
----
917+
1 2 NULL
918+
919+
statement ok
920+
CLOSE curs;
921+
922+
subtest end

pkg/sql/sql_cursor.go

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -445,54 +445,67 @@ const (
445445
)
446446

447447
func (c *cursorMap) closeAll(p *planner, reason cursorCloseReason) error {
448-
for n, curs := range c.cursors {
449-
switch reason {
450-
case cursorCloseForTxnCommit:
451-
if curs.withHold {
452-
// Cursors declared using WITH HOLD are not closed at transaction
453-
// commit, and become the responsibility of the session.
454-
curs.committed = true
455-
if !curs.persisted {
456-
// Execute the cursor's query to completion and persist the result so
457-
// that it can survive the transaction's commit.
458-
if err := persistCursor(p, curs); err != nil {
459-
return err
460-
}
448+
if reason == cursorCloseForTxnCommit {
449+
for _, curs := range c.cursors {
450+
if curs.withHold && !curs.persisted {
451+
// Execute the cursor's query to completion and persist the result so
452+
// that it can survive the transaction's commit. The cursor will become
453+
// the responsibility of the session.
454+
if err := persistCursor(p, curs); err != nil {
455+
return errors.CombineErrors(err, c.closeAll(p, cursorCloseForTxnRollback))
461456
}
462-
continue
463-
}
464-
case cursorCloseForTxnRollback:
465-
if curs.committed {
466-
// Transaction rollback should only remove cursors that were created in
467-
// the current transaction.
468-
continue
469-
}
470-
case cursorCloseForTxnPrepare:
471-
if curs.withHold && !curs.committed {
472-
// Disallow preparing a transaction that has created a cursor WITH HOLD.
473-
// It's fine for previous transactions to have created holdable cursors.
474-
// NOTE: Postgres also disallows this.
475-
//
476-
// Make sure to close all cursors before returning the error.
477-
err := c.closeAll(p, cursorCloseForTxnRollback)
478-
return errors.CombineErrors(
479-
pgerror.New(pgcode.FeatureNotSupported,
480-
"cannot PREPARE a transaction that has created a cursor WITH HOLD"),
481-
err,
482-
)
483457
}
484458
}
485-
if err := curs.Close(); err != nil {
486-
return err
459+
}
460+
// Close the cursor iterators/containers. Make sure to continue closing even
461+
// if one of them fails.
462+
var retErr error
463+
for _, curs := range c.cursors {
464+
if reason != cursorCloseForExplicitClose && curs.committed {
465+
// A holdable cursor from a previously committed transaction should remain
466+
// open, except for a session close.
467+
continue
468+
}
469+
if reason == cursorCloseForTxnCommit && curs.withHold {
470+
// A holdable cursor remains open after transaction commit.
471+
continue
487472
}
488-
delete(c.cursors, n)
473+
if reason == cursorCloseForTxnPrepare && curs.withHold && !curs.committed {
474+
// Disallow preparing a transaction that has created a cursor WITH HOLD.
475+
// It's fine for previous transactions to have created holdable cursors.
476+
// NOTE: Postgres also disallows this.
477+
//
478+
// Make sure to close all cursors before returning the error.
479+
err := c.closeAll(p, cursorCloseForTxnRollback)
480+
return errors.CombineErrors(
481+
pgerror.New(pgcode.FeatureNotSupported,
482+
"cannot PREPARE a transaction that has created a cursor WITH HOLD"),
483+
err,
484+
)
485+
}
486+
retErr = errors.CombineErrors(retErr, curs.Close())
487+
}
488+
if reason == cursorCloseForTxnCommit && retErr != nil {
489+
return errors.CombineErrors(retErr, c.closeAll(p, cursorCloseForTxnRollback))
489490
}
491+
// Remove closed cursors from the map. Mark holdable cursors as committed
492+
// for a transaction commit.
490493
if reason == cursorCloseForExplicitClose {
491494
// All cursors are closed for explicit close, so we can lose the reference
492495
// to the map.
493496
c.cursors = nil
497+
} else {
498+
for n, curs := range c.cursors {
499+
if reason == cursorCloseForTxnCommit && curs.withHold {
500+
curs.committed = true
501+
}
502+
if curs.committed {
503+
continue
504+
}
505+
delete(c.cursors, n)
506+
}
494507
}
495-
return nil
508+
return retErr
496509
}
497510

498511
func (c *cursorMap) closeCursor(s tree.Name) error {
@@ -583,7 +596,7 @@ func (p *planner) checkNoConflictingCursors(stmt tree.Statement) error {
583596

584597
// persistCursor runs the given cursor to completion and stores the result in a
585598
// row container that can outlive the cursor's transaction.
586-
func persistCursor(p *planner, cursor *sqlCursor) error {
599+
func persistCursor(p *planner, cursor *sqlCursor) (retErr error) {
587600
// Use context.Background() because the cursor can outlive the context in
588601
// which it was created.
589602
helper := persistedCursorHelper{
@@ -602,6 +615,12 @@ func persistCursor(p *planner, cursor *sqlCursor) error {
602615
p.ExtendedEvalContextCopy(),
603616
"persisted_cursor", /* opName */
604617
)
618+
defer func() {
619+
if retErr != nil {
620+
// Close the container if we encountered an error.
621+
helper.container.Close(helper.ctx)
622+
}
623+
}()
605624
for {
606625
ok, err := cursor.Next(helper.ctx)
607626
if err != nil {
@@ -619,6 +638,7 @@ func persistCursor(p *planner, cursor *sqlCursor) error {
619638
return err
620639
}
621640
cursor.Rows = &helper
641+
cursor.persisted = true
622642
return nil
623643
}
624644

0 commit comments

Comments
 (0)