{"record":{"id":"e1a0aa2a2e7062dd","repo":"gastownhall/beads","slug":"rollback-checked-close-savepoint-w","errorCode":null,"errorMessage":"rollback checked close savepoint: %w","messagePattern":"rollback checked close savepoint: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":251,"sourceCode":"func createCloseCheckedSavepoint(ctx context.Context, tx DBTX) (string, error) {\n\tname := closeCheckedSavepointPrefix + strconv.FormatUint(closeCheckedSavepointCounter.Add(1), 10)\n\t//nolint:gosec // G201: name is a fixed identifier-safe prefix plus an atomic decimal counter.\n\tif _, err := tx.ExecContext(ctx, \"SAVEPOINT \"+name); err != nil {\n\t\treturn \"\", fmt.Errorf(\"create checked close savepoint: %w\", err)\n\t}\n\treturn name, nil\n}\n\nfunc rollbackAndReleaseCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {\n\trollbackErr := rollbackToCloseCheckedSavepoint(ctx, tx, name)\n\treleaseErr := releaseCloseCheckedSavepoint(ctx, tx, name)\n\treturn errors.Join(rollbackErr, releaseErr)\n}\n\nfunc rollbackToCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {\n\t//nolint:gosec // G201: name is generated by createCloseCheckedSavepoint.\n\tif _, err := tx.ExecContext(ctx, \"ROLLBACK TO SAVEPOINT \"+name); err != nil {\n\t\treturn fmt.Errorf(\"rollback checked close savepoint: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc releaseCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {\n\t//nolint:gosec // G201: name is generated by createCloseCheckedSavepoint.\n\tif _, err := tx.ExecContext(ctx, \"RELEASE SAVEPOINT \"+name); err != nil {\n\t\treturn fmt.Errorf(\"release checked close savepoint: %w\", err)\n\t}\n\treturn nil\n}\n\n// dependencyTargetColumnForIDInTx returns the typed target column for id,\n// preferring a durable row when an id is present in both tables.\nfunc dependencyTargetColumnForIDInTx(ctx context.Context, tx DBTX, id string) (string, error) {\n\tvar found int\n\terr := tx.QueryRowContext(ctx, \"SELECT 1 FROM issues WHERE id = ?\", id).Scan(&found)\n\tif err == nil {","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L233-L269","documentation":"Beads throws this when 'ROLLBACK TO SAVEPOINT <name>' fails after a checked close trial determined the close could not proceed (e.g. open children). Rolling back the savepoint undoes the trial-run writes; failing here leaves the transaction in an indeterminate state, so the caller joins this error with the release error and aborts.","triggerScenarios":"CloseIssueCheckedInTx (or rollbackAndReleaseCloseCheckedSavepoint) invoking ROLLBACK TO SAVEPOINT when: the savepoint no longer exists (released or outer rollback already occurred), the transaction was already terminated by a prior error (e.g. deadlock or lock timeout), or the connection dropped mid-transaction.","commonSituations":"A prior statement in the transaction caused an implicit transaction rollback (deadlock, serialization failure), making the savepoint vanish; another layer released the same savepoint; connection pool reclaiming a broken connection.","solutions":["Check for a preceding error in the same transaction (deadlock/lock timeout) — that original failure is the root cause; fix or retry for it","Retry the whole close operation; transactional semantics make a full retry safe after a connection-level rollback","Avoid nesting beads transactions with external savepoint management on the same connection","If using a custom driver, verify it preserves savepoints after statement-level errors"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Ensure no prior statement has poisoned the transaction before the trial\nif err := tx.QueryRowContext(ctx, \"SELECT 1\").Scan(&one); err != nil {\n\treturn fmt.Errorf(\"transaction already aborted, savepoint rollback will fail: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"if err := CloseIssueCheckedInTx(ctx, tx, id, opts); err != nil {\n\tvar joined interface{ Unwrap() []error }\n\tif errors.As(err, &joined) {\n\t\tfor _, sub := range joined.Unwrap() {\n\t\t\tif dberrors.IsDeadlock(sub) || dberrors.IsLockTimeout(sub) {\n\t\t\t\treturn retryClose(ctx, id) // root cause was the earlier statement\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}","preventionTips":["On deadlock/lock-timeout, retry the entire transaction — the savepoint no longer exists after implicit rollback","Do not RELEASE the same savepoint twice in your own code around beads calls","Keep beads' savepoint lifecycle internal; don't interleave manual savepoint SQL on the same tx","Detect deadlocks early and back off with jitter before retrying"],"tags":["sql","savepoint","transaction","rollback"],"backgroundTag":"savepoint-rollback-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}