gastownhall/beads · error

create checked close savepoint: %w

Error message

create checked close savepoint: %w

What it means

Beads throws this when issuing 'SAVEPOINT <name>' fails during a checked close. CloseIssueCheckedInTx creates a savepoint so it can trial-run the close policy and roll back if children are still open. The savepoint name is internally generated (fixed prefix + atomic counter), so this error almost always reflects a database/engine limitation rather than bad input.

Source

Thrown at internal/storage/issueops/close.go:237

		  AND child.status != 'closed'
		  AND NOT EXISTS (
			SELECT 1 FROM dependencies AS durable WHERE durable.id = dependency.id
		  )
	`, targetColumn)
	if err := tx.QueryRowContext(ctx, wispQuery, id).Scan(&wispCount); err != nil {
		if optionalBlockedTable("wisp_dependencies") && isTableNotExistError(err) {
			return durableCount, nil
		}
		return 0, fmt.Errorf("count open wisp children for %s: %w", id, err)
	}
	return durableCount + wispCount, nil
}

func createCloseCheckedSavepoint(ctx context.Context, tx DBTX) (string, error) {
	name := closeCheckedSavepointPrefix + strconv.FormatUint(closeCheckedSavepointCounter.Add(1), 10)
	//nolint:gosec // G201: name is a fixed identifier-safe prefix plus an atomic decimal counter.
	if _, err := tx.ExecContext(ctx, "SAVEPOINT "+name); err != nil {
		return "", fmt.Errorf("create checked close savepoint: %w", err)
	}
	return name, nil
}

func rollbackAndReleaseCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {
	rollbackErr := rollbackToCloseCheckedSavepoint(ctx, tx, name)
	releaseErr := releaseCloseCheckedSavepoint(ctx, tx, name)
	return errors.Join(rollbackErr, releaseErr)
}

func rollbackToCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {
	//nolint:gosec // G201: name is generated by createCloseCheckedSavepoint.
	if _, err := tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT "+name); err != nil {
		return fmt.Errorf("rollback checked close savepoint: %w", err)
	}
	return nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify the storage backend and driver support SAVEPOINT inside a transaction (Dolt/MySQL do; ensure driver version is current)
  2. Confirm the tx passed in is a live transaction — savepoints require an active transaction context
  3. Upgrade the beads storage driver / Dolt if the error indicates unsupported statement syntax
  4. Check connection health; reconnect and retry the close if the connection dropped
Defensive patterns

Strategy: fallback

Validate before calling

// Probe savepoint support before relying on checked close
if _, err := tx.ExecContext(ctx, "SAVEPOINT probe_beads_support"); err != nil {
	return fmt.Errorf("backend lacks SAVEPOINT support: %w", err)
}
_, _ = tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT probe_beads_support")
_, _ = tx.ExecContext(ctx, "RELEASE SAVEPOINT probe_beads_support")

Try / catch

name, err := createCloseCheckedSavepoint(ctx, tx)
if err != nil {
	if dberrors.IsConnectionError(err) {
		return retryClose(ctx, id)
	}
	// Fallback: use plain (unchecked) close if the backend cannot savepoint
	log.Printf("savepoint unavailable (%v); falling back to unchecked close", err)
	return closeUnchecked(ctx, tx, id)
}

Prevention

When it happens

Trigger: Calling CloseIssue with check semantics when the underlying engine rejects SAVEPOINT: transaction not active (autocommit driver wrapper), engine without savepoint support, connection lost between BEGIN and SAVEPOINT, or too many nested savepoints.

Common situations: Using a driver/backend that doesn't support MySQL-style SAVEPOINT (some SQLite wrappers, older Dolt versions); a pooled connection whose transaction was already terminated; long transactions hitting engine savepoint-depth limits.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/4ba5a992899d8d98. Report an issue: GitHub.