{"record":{"id":"7d142802ca085d70","repo":"gastownhall/beads","slug":"release-checked-close-savepoint-v-rollback-to-s","errorCode":null,"errorMessage":"release checked close savepoint: %v; rollback to savepoint: %w","messagePattern":"release checked close savepoint: (.+?); rollback to savepoint: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/issueops/close.go","lineNumber":101,"sourceCode":"\t// statements cannot retain a savepoint. The shared UOW uses a pinned\n\t// *sql.Conn after START TRANSACTION, while embedded callers use *sql.Tx.\n\tif !closeCheckedSavepointEligible(tx) {\n\t\treturn closeIssueCheckedAfterSavepoint(ctx, tx, id, reason, actor, session, force, closed, targetColumn)\n\t}\n\tsavepoint, err := createCloseCheckedSavepoint(ctx, tx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult, scopedErr := closeIssueCheckedAfterSavepoint(ctx, tx, id, reason, actor, session, force, closed, targetColumn)\n\tif scopedErr != nil {\n\t\tif cleanupErr := rollbackAndReleaseCloseCheckedSavepoint(ctx, tx, savepoint); cleanupErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"discard checked close savepoint after %v: %w\", scopedErr, cleanupErr)\n\t\t}\n\t\treturn nil, scopedErr\n\t}\n\tif err := releaseCloseCheckedSavepoint(ctx, tx, savepoint); err != nil {\n\t\tif rollbackErr := rollbackToCloseCheckedSavepoint(ctx, tx, savepoint); rollbackErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"release checked close savepoint: %v; rollback to savepoint: %w\", err, rollbackErr)\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn result, nil\n}\n\nfunc closeCheckedSavepointEligible(tx DBTX) bool {\n\tswitch tx.(type) {\n\tcase *sql.Tx, *sql.Conn:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc closeIssueCheckedAfterSavepoint(ctx context.Context, tx DBTX, id, reason, actor, session string, force, closed bool, targetColumn string) (*CloseResult, error) {\n\topenChildren, err := enforceClosePolicyForTargetInTx(ctx, tx, id, targetColumn, force, closed)\n\tif err != nil {","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L83-L119","documentation":"The checked close itself succeeded, but RELEASE SAVEPOINT failed, and the fallback ROLLBACK TO SAVEPOINT also failed. The close work may be in an ambiguous savepoint state inside the transaction, so the library returns this compound error (release error as %v context, rollback error wrapped via %w) instead of pretending the transaction is clean.","triggerScenarios":"createCloseCheckedSavepoint succeeded and the close completed, but both RELEASE SAVEPOINT and ROLLBACK TO SAVEPOINT error — nearly always a dead connection, canceled context, or a transaction the driver has already aborted.","commonSituations":"Server restart or network partition right after the close statement; a *sql.DB Runner passed where a savepoint-capable *sql.Tx/*sql.Conn is expected (that path is normally gated by closeCheckedSavepointEligible, but custom DBTX wrappers can evade it).","solutions":["Roll back the entire transaction; do not attempt to commit it","Read the embedded release error for the root cause (connection/context)","Retry the close in a new transaction — the previous one is discarded","If using a custom DBTX wrapper, ensure it is *sql.Tx or *sql.Conn so savepoint logic is eligible, or avoid the savepoint path entirely"],"exampleFix":"if err != nil && strings.Contains(err.Error(), \"rollback to savepoint\") {\n\t_ = tx.Rollback() // do not commit — savepoint state is unknown\n\treturn retryCloseInFreshTx(ctx, id)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"result, err := store.CloseIssue(ctx, id, reason, actor)\nif err != nil && strings.Contains(err.Error(), \"rollback to savepoint\") {\n\t_ = tx.Rollback() // savepoint state unknown — abandon the transaction\n\treturn retryInFreshTransaction(ctx, id)\n}","preventionTips":["Treat any savepoint error as transaction-fatal; never commit afterward","Keep connections alive (avoid mid-operation network churn and context cancels)","Pass real *sql.Tx/*sql.Conn handles so the eligible fast path avoids savepoints only when correct"],"tags":["database","savepoint","transaction"],"backgroundTag":"savepoint-release-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}